/* 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 "input/input_driver.h"
#include
#include
#include
#include
#include
#include
#ifdef HAVE_NETWORKING
#include
#include
#endif
#include
#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"
#include "input/input_remapping.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.h"
#include "core_info.h"
#include "playlist.h"
#include
#include
#include "cheat_manager.h"
#include "content.h"
#include "dynamic.h"
#include "file_path_special.h"
#include "list_special.h"
#include "paths.h"
#include "record/record_driver.h"
#include "retroarch.h"
#include "runloop.h"
#include "verbosity.h"
#include "version.h"
#include "version_git.h"
#include "tasks/task_content.h"
#ifdef HAVE_TRANSLATE
#include "accessibility.h"
#endif
#ifdef HAVE_SCREENSHOTS
#include "tasks/tasks_internal.h"
#endif
#include
#ifdef __MACH__
#include
#endif
#define CMD_BUF_SIZE 4096
/* Replies with a failure and why, and marks the reply as one: a line
* client reads the text, a structured one is told outright. The text
* opens with @name and ERROR, as a deferred reply's does. */
void command_reply_error(command_t *cmd, const char *name,
const char *fmt, ...)
{
char msg[512];
int n;
size_t _len;
va_list ap;
if (!cmd || !cmd->replier)
return;
/* Both of these report the length they wanted rather than the one
* they wrote, so each is brought back inside the buffer before the
* next writes past its end. */
if ((n = snprintf(msg, sizeof(msg), "%s ERROR ", name)) < 0)
return;
_len = (size_t)n;
if (_len >= sizeof(msg))
_len = sizeof(msg) - 1;
va_start(ap, fmt);
n = vsnprintf(msg + _len, sizeof(msg) - _len, fmt, ap);
va_end(ap);
if (n > 0)
{
_len += (size_t)n;
if (_len >= sizeof(msg))
_len = sizeof(msg) - 1;
}
cmd->error = true;
cmd->replier(cmd, msg, _len);
}
#ifdef HAVE_COMMAND
/* a queued STEPS was dropped since the reply now sent began */
static bool command_queue_ended;
#endif
static void command_post_state_loaded(void)
{
#ifdef HAVE_COMMAND
/* steps queued from the state it left would play on from this one */
command_run_frames_forget();
#endif
#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;
/* past the separating space; a bare command gets "", not the
* byte after its terminator */
if (arg)
*arg = *argument ? argument + 1 : argument;
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;
handle->error = false;
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;
char *tok = strtok_r(buf, "\n", &save);
unsigned gen = input_driver_command_generation();
while (tok)
{
/* a client ending its lines in CRLF leaves no \r on the last word;
* a \r anywhere else is the command's own */
size_t len = strlen(tok);
if (len && tok[len - 1] == '\r')
tok[len - 1] = '\0';
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, (int)len, 0,
(struct sockaddr*)&netcmd->cmd_source, netcmd->cmd_source_len);
}
typedef struct
{
struct sockaddr_storage addr;
socklen_t addr_len;
} command_network_dest_t;
static void *network_command_reply_dest(command_t *cmd)
{
command_network_t *netcmd = (command_network_t*)cmd->userptr;
command_network_dest_t *dest = (command_network_dest_t*)
malloc(sizeof(*dest));
if (dest)
{
dest->addr = netcmd->cmd_source;
dest->addr_len = netcmd->cmd_source_len;
}
return dest;
}
static void network_command_reply_to(command_t *cmd, void *data,
const char *s, size_t len)
{
command_network_t *netcmd = (command_network_t*)cmd->userptr;
command_network_dest_t *dest = (command_network_dest_t*)data;
sendto(netcmd->net_fd, s, (int)len, 0,
(struct sockaddr*)&dest->addr, dest->addr_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
|| command_interfaces_held())
return;
}
}
command_t* command_network_new(uint16_t port, const char *bind_address)
{
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;
/* An empty bind address means every interface (NULL server ->
* AI_PASSIVE -> 0.0.0.0), which is what this interface has always
* done. Anyone on that network can then send LOAD_CORE or
* WRITE_CORE_RAM, so users who only drive RetroArch from the same
* machine can set network_cmd_bind_address = "127.0.0.1". */
if (bind_address && !*bind_address)
bind_address = NULL;
fd = socket_init((void**)&res, port, bind_address,
SOCKET_TYPE_DATAGRAM, AF_INET);
RARCH_LOG("[NetCMD] %s %hu (%s).\n",
msg_hash_to_str(MSG_BRINGING_UP_COMMAND_INTERFACE_ON_PORT),
(unsigned short)port,
bind_address ? bind_address : "all interfaces");
if (fd < 0)
goto error;
netcmd->net_fd = fd;
cmd->userptr = netcmd;
cmd->poll = command_network_poll;
cmd->replier = network_command_reply;
cmd->reply_dest = network_command_reply_dest;
cmd->reply_to = network_command_reply_to;
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
#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];
/* Which connection holds each slot: a closed descriptor's number
* is handed out again, so the number alone does not say. */
unsigned serial[MAX_USER_CONNECTIONS];
unsigned next_serial;
/* Last received user socket */
int last_fd;
unsigned last_serial;
} command_uds_t;
typedef struct
{
int fd;
unsigned serial;
} command_uds_dest_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_reply_dest(command_t *cmd)
{
command_uds_t *subcmd = (command_uds_t*)cmd->userptr;
command_uds_dest_t *dest = (command_uds_dest_t*)malloc(sizeof(*dest));
if (dest)
{
dest->fd = subcmd->last_fd;
dest->serial = subcmd->last_serial;
}
return dest;
}
/* Only to the connection that asked: gone, the reply is dropped. */
static void uds_command_reply_to(command_t *cmd, void *data,
const char *s, size_t len)
{
int i;
command_uds_t *subcmd = (command_uds_t*)cmd->userptr;
command_uds_dest_t *dest = (command_uds_dest_t*)data;
for (i = 0; i < MAX_USER_CONNECTIONS; i++)
if ( subcmd->userfd[i] == dest->fd
&& subcmd->serial[i] == dest->serial)
{
write(dest->fd, s, len);
return;
}
}
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;
udscmd->last_serial = udscmd->serial[i];
command_parse_msg(handle, buf);
/* See command_generation: this object may be gone now. */
if ( input_driver_command_generation() != gen
|| command_interfaces_held())
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;
udscmd->serial[i] = ++udscmd->next_serial;
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->reply_dest = uds_command_reply_dest;
cmd->reply_to = uds_command_reply_to;
cmd->destroy = uds_command_free;
return cmd;
}
#endif
/* Routines used to invoke retroarch command ... */
/* The family tool a command runs for, so its answers carry the tool's
* name: STATE save 1 rather than SAVE_STATE_SLOT 1. */
struct command_family
{
const char *family;
const char *word;
const char *name;
command_replier_t replier;
/* the command answered, or will */
bool answered;
};
static struct command_family command_family_now;
/* @msg under @f's name, allocated, its length in @out_len; NULL where
* it could not be, or where it is to go as it is. */
static char *command_family_rename(const struct command_family *f,
const char *msg, size_t len, size_t *out_len)
{
size_t n = strlen(f->name);
size_t size = len + strlen(f->family) + strlen(f->word) + 3;
char *s = (char*)malloc(size);
int _len;
if (!s)
return NULL;
if ( len > n && !strncmp(msg, f->name, n)
&& (msg[n] == ' ' || msg[n] == '\n' || msg[n] == ','))
{
msg += n;
len -= n;
if (!strncmp(msg, " ERROR", 6))
_len = snprintf(s, size, "%s%.*s", f->family, (int)len, msg);
else
_len = snprintf(s, size, "%s %s%.*s", f->family, f->word,
(int)len, msg);
}
else if (len == n && !strncmp(msg, f->name, n))
_len = snprintf(s, size, "%s %s", f->family, f->word);
/* rows, which name nothing, are the tool's answer as they are */
else
{
free(s);
return NULL;
}
if (_len <= 0 || (size_t)_len >= size)
{
free(s);
return NULL;
}
*out_len = (size_t)_len;
return s;
}
static void command_family_replier(command_t *cmd, const char *data,
size_t len)
{
size_t _len = 0;
char *msg = command_family_rename(&command_family_now, data, len,
&_len);
command_family_now.answered = true;
if (msg)
command_family_now.replier(cmd, msg, _len);
else
command_family_now.replier(cmd, data, len);
free(msg);
}
/* A reply sent after its command has returned: where it goes is taken
* while the command runs, as afterwards the interface may be answering
* someone else. */
typedef struct
{
command_t *cmd;
void *dest;
struct command_family family;
unsigned gen;
} command_deferred_t;
static void command_deferred_take(command_deferred_t *d, command_t *cmd)
{
d->cmd = cmd;
d->dest = cmd->reply_dest ? cmd->reply_dest(cmd) : NULL;
d->gen = input_driver_command_generation();
d->family = command_family_now;
if (command_family_now.family)
command_family_now.answered = true;
}
/* Sends @msg (none when NULL) and releases @d. Dropped, unsent, if the
* interface was torn down meanwhile (the command generation moved).
* @is_error says the reply reports a failure, so the interface sending
* it is told rather than left to read the text. */
static void command_deferred_send(command_deferred_t *d,
const char *msg, size_t len, bool is_error)
{
char *renamed = NULL;
if (msg && input_driver_command_generation() == d->gen)
{
size_t _len = 0;
if (d->family.family && (renamed = command_family_rename(&d->family,
msg, len, &_len)))
{
msg = renamed;
len = _len;
}
d->cmd->error = is_error;
if (d->cmd->reply_to && d->dest)
d->cmd->reply_to(d->cmd, d->dest, msg, len);
else if (!d->cmd->reply_dest)
d->cmd->replier(d->cmd, msg, len);
}
free(renamed);
free(d->dest);
d->dest = NULL;
}
#ifdef HAVE_SCREENSHOTS
/* SCREENSHOT's answer, and GET_FRAME's - the frame written nowhere, so
* with no path: the picture where the interface takes one, with the
* file's path or the frame's size as its text. */
static void command_screenshot_done(retro_task_t *task, void *task_data,
void *user_data, const char *error)
{
command_deferred_t *d = (command_deferred_t*)user_data;
const struct screenshot_result *r = (const struct screenshot_result*)task_data;
bool frame = r && !*r->path;
char msg[PATH_MAX_LENGTH];
if (error || !r || (frame && !r->png_base64))
{
snprintf(msg, sizeof(msg), "%s ERROR %s",
frame ? "GET_FRAME" : "SCREENSHOT",
error ? error : "no result");
command_deferred_send(d, msg, strlen(msg), true);
}
else
{
if (frame)
snprintf(msg, sizeof(msg), "GET_FRAME %ux%u", r->width, r->height);
else
strlcpy(msg, r->path, sizeof(msg));
if ( r->png_base64 && d->cmd->reply_image_to && d->dest
&& input_driver_command_generation() == d->gen)
{
d->cmd->reply_image_to(d->cmd, d->dest, msg, strlen(msg),
"image/png", r->png_base64, r->png_base64_len);
command_deferred_send(d, NULL, 0, false);
}
else
command_deferred_send(d, msg, strlen(msg), false);
}
free(d);
}
/* SCREENSHOT run by command_run(): answered with the file's path once
* the screenshot task has written it. */
static bool command_screenshot(command_t *cmd)
{
settings_t *settings = config_get_ptr();
runloop_state_t *runloop_st = runloop_state_get_ptr();
command_deferred_t *d = (command_deferred_t*)calloc(1, sizeof(*d));
if (!d)
return false;
/* Threaded (use_thread true), so the callback, which reads @d, runs
* from the task queue on a later frame, after it is taken below.
* Unthreaded, the callback would run inside this call and free @d
* before it was taken. It is taken only once the screenshot is
* queued, as taking it marks the asking MCP request as waiting. */
if (!take_screenshot_notify(settings->paths.directory_screenshot,
runloop_st->runtime_content_path_basename, false,
video_driver_cached_frame_is_hw_render(), false, true,
command_screenshot_done, d))
{
free(d);
return false;
}
/* the task's callback, which uses this, runs on a later frame */
command_deferred_take(d, cmd);
return true;
}
#ifdef HAVE_RPNG
/* GET_FRAME [size]: the frame on screen as image content, scaled to fit
* a square of @size and never up. Nothing is written: a software core's
* frame is copied, a hardware core's read back from the GPU. */
bool command_get_frame(command_t *cmd, const char *arg)
{
command_deferred_t *d;
unsigned long size = COMMAND_FRAME_SIZE;
char *end = NULL;
while (arg && *arg == ' ')
arg++;
if (arg && *arg)
{
size = strtoul(arg, &end, 10);
if ( *arg < '0' || *arg > '9' || *end
|| size < COMMAND_FRAME_SIZE_MIN || size > COMMAND_FRAME_SIZE_MAX)
{
command_reply_error(cmd, "GET_FRAME", "a size from %u to %u",
COMMAND_FRAME_SIZE_MIN, COMMAND_FRAME_SIZE_MAX);
return false;
}
}
/* the line interfaces carry text alone */
if (!cmd->reply_image_to)
{
command_reply_error(cmd, "GET_FRAME", "this interface carries no "
"image; SCREENSHOT writes one to a file");
return false;
}
if (!(runloop_get_flags() & RUNLOOP_FLAG_CORE_RUNNING))
{
command_reply_error(cmd, "GET_FRAME", "no content is running");
return false;
}
if (!(d = (command_deferred_t*)calloc(1, sizeof(*d))))
return false;
if (!take_frame_notify((unsigned)size, (unsigned)size,
command_screenshot_done, d))
{
free(d);
command_reply_error(cmd, "GET_FRAME", "the frame could not be "
"taken: none has been drawn, or a screenshot is being taken");
return false;
}
command_deferred_take(d, cmd);
return true;
}
#endif
#endif
/* A command answered once the work it starts is through: by @ok, else
* by @name and the task's result, or by @name, ERROR and the reason. */
struct command_reply
{
command_deferred_t deferred;
enum event_command event;
/* Run instead of the event, when set; false has the failure
* reported for it. */
bool (*start)(struct command_reply *r);
const char *name;
char ok[64];
char path[PATH_MAX_LENGTH];
/* a load that ends on the dummy core failed */
bool want_core;
/* LOAD_CONTENT ... paused: pause once it has loaded */
bool pause;
/* what succeeded loaded a state */
bool state_loaded;
bool reset;
};
static struct command_reply *command_reply_new(const char *name)
{
struct command_reply *r = (struct command_reply*)calloc(1, sizeof(*r));
if (r)
r->name = name;
return r;
}
static void command_reply_done(retro_task_t *task, void *task_data,
void *user_data, const char *error)
{
struct command_reply *r = (struct command_reply*)user_data;
char msg[PATH_MAX_LENGTH + 128];
/* a core that fails to start is replaced by the dummy core, and the
* load goes on to succeed */
if ( !error && r->want_core
&& runloop_state_get_ptr()->current_core_type == CORE_TYPE_DUMMY)
error = msg_hash_to_str(MSG_FAILED_TO_LOAD_CONTENT);
if (error)
snprintf(msg, sizeof(msg), "%s ERROR %s", r->name, error);
else
{
if (*r->ok)
strlcpy(msg, r->ok, sizeof(msg));
else if (task_data)
snprintf(msg, sizeof(msg), "%s %s", r->name, (const char*)task_data);
else
strlcpy(msg, r->name, sizeof(msg));
if (r->state_loaded)
command_post_state_loaded();
#ifdef HAVE_COMMAND
if ( command_queue_ended
&& (r->state_loaded || r->reset))
strlcat(msg, ", queue ended", sizeof(msg));
#endif
/* a pause is dropped with no core running yet, as after a cold
* start: then it is the first frame's to do */
if (r->pause)
{
if (runloop_get_flags() & RUNLOOP_FLAG_CORE_RUNNING)
command_event(CMD_EVENT_PAUSE, NULL);
else
runloop_frame_work_set(RUNLOOP_WORK_PAUSE_LOADED, true);
}
}
command_deferred_send(&r->deferred, msg, strlen(msg), error != NULL);
free(r);
}
/* Runs the work from a main-thread task, outside the core's run: a
* command can arrive from the core's own input poll. */
static void command_reply_handler(retro_task_t *task)
{
struct command_reply *r = (struct command_reply*)task->state;
task_notify_t notify;
task->state = NULL;
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
notify.cb = command_reply_done;
notify.user_data = r;
#ifdef HAVE_COMMAND
command_queue_ended = false;
#endif
if (!(r->start ? r->start(r) : command_event(r->event, ¬ify)))
command_reply_done(NULL, NULL, r, "The command failed.");
}
/* Takes @r, NULL included. */
static bool command_reply_push(command_t *cmd, struct command_reply *r)
{
retro_task_t *task;
if (!r)
return false;
if (!(task = task_init()))
{
free(r);
return false;
}
task->handler = command_reply_handler;
task->state = r;
task->flags |= RETRO_TASK_FLG_MAIN_THREAD | RETRO_TASK_FLG_MUTE;
if (!task_queue_push(task))
{
free(task);
free(r);
return false;
}
command_deferred_take(&r->deferred, cmd);
return true;
}
#ifdef HAVE_BSV_MOVIE
/* Pushes @event as the work of command @name. */
static bool command_reply_event(command_t *cmd, enum event_command event,
const char *name)
{
struct command_reply *r = command_reply_new(name);
if (r)
r->event = event;
return command_reply_push(cmd, r);
}
#endif
#ifdef HAVE_TRANSLATE
/* AI_SERVICE run by command_run(): one translation of the screen,
* answered with the service's text. */
static bool command_ai_service_start(struct command_reply *r)
{
settings_t *settings = config_get_ptr();
if (!settings->bools.ai_service_enable)
return false;
return run_translation_service_notify(settings,
(runloop_get_flags() & RUNLOOP_FLAG_PAUSED) != 0,
command_reply_done, r);
}
#endif
/* Answers @cmd once the content load in progress, or with @next the
* one the command leads to, is through. */
static bool command_content_reply(command_t *cmd, const char *name,
bool next, bool want_core, bool pause)
{
struct command_reply *r;
/* a line client is answered too where it asked for a pause, which
* happens here */
if ( (!cmd->structured && !pause)
|| (!next && !task_content_load_pending())
|| !(r = command_reply_new(name)))
return true;
r->want_core = want_core;
r->pause = pause;
task_notify_set(&content_state_get_ptr()->load_notify,
command_reply_done, r);
command_deferred_take(&r->deferred, cmd);
return true;
}
/* Runs @name with @arg (NULL or "" for none) as if it had arrived as a
* command line on @handle: replies go to @handle's replier, a hotkey
* presses on @handle for one frame. For interfaces that receive
* commands as structured requests rather than lines.
*
* Returns: false for an unknown command or one whose handler failed. */
bool command_run(command_t *handle, const char *name, const char *arg)
{
unsigned i;
handle->error = false;
for (i = 0; i < ARRAY_SIZE(action_map); i++)
if (string_is_equal(name, action_map[i].str))
return action_map[i].action(handle, arg ? arg : "");
for (i = 0; i < ARRAY_SIZE(map); i++)
if (string_is_equal(name, map[i].str))
{
if (map[i].id == RARCH_MENU_TOGGLE)
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
#ifdef HAVE_SCREENSHOTS
else if (map[i].id == RARCH_SCREENSHOT)
return command_screenshot(handle);
#endif
#ifdef HAVE_BSV_MOVIE
else if (map[i].id == RARCH_RECORD_REPLAY_KEY)
return command_reply_event(handle,
CMD_EVENT_RECORD_REPLAY, map[i].str);
else if (map[i].id == RARCH_SAVE_REPLAY_CHECKPOINT_KEY)
return command_reply_event(handle,
CMD_EVENT_SAVE_REPLAY_CHECKPOINT, map[i].str);
else if (map[i].id == RARCH_PREV_REPLAY_CHECKPOINT_KEY)
return command_reply_event(handle,
CMD_EVENT_PREV_REPLAY_CHECKPOINT, map[i].str);
else if (map[i].id == RARCH_NEXT_REPLAY_CHECKPOINT_KEY)
return command_reply_event(handle,
CMD_EVENT_NEXT_REPLAY_CHECKPOINT, map[i].str);
#endif
#ifdef HAVE_TRANSLATE
else if (map[i].id == RARCH_AI_SERVICE)
{
struct command_reply *r = command_reply_new(map[i].str);
if (r)
r->start = command_ai_service_start;
return command_reply_push(handle, r);
}
#endif
else
handle->state[map[i].id] = true;
/* The press goes out on the next poll and the hotkey acts a
* frame later, so there is nothing yet to report but that it
* was sent. A structured client is told that much rather than
* a bare "done": what the hotkey changed is read back with
* GET_OPTION. MENU_TOGGLE is the one that has already acted,
* above, and reports the menu instead. */
if (handle->structured)
{
char reply[NAME_MAX_LENGTH + 16];
size_t _len;
#ifdef HAVE_MENU
if (map[i].id == RARCH_MENU_TOGGLE)
{
struct menu_state *menu_st = menu_state_get_ptr();
_len = (size_t)snprintf(reply, sizeof(reply), "%s %s\n",
map[i].str,
(menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE))
? "open" : "closed");
}
else
#endif
_len = (size_t)snprintf(reply, sizeof(reply), "%s pressed\n",
map[i].str);
handle->replier(handle, reply, _len);
}
return true;
}
return false;
}
/* MENU, DISK, REINIT, STATE, REPLAY and CORE: a family of commands as one,
* so a structured client is offered one tool with a choice rather than
* a tool apiece. Each word runs as the command it stands for, which
* line clients can still send by name; @rest says it takes what
* follows the word, a slot or a frame. */
struct cmd_choice
{
const char *word;
const char *name;
bool rest;
};
static bool command_run_choice(command_t *cmd, const char *family,
const struct cmd_choice *choices, size_t count, const char *values,
const char *arg)
{
size_t i;
size_t n;
const char *rest;
while (arg && *arg == ' ')
arg++;
if (!arg)
arg = "";
for (rest = arg; *rest && *rest != ' '; rest++)
;
n = (size_t)(rest - arg);
while (*rest == ' ')
rest++;
for (i = 0; i < count; i++)
{
if (strncmp(arg, choices[i].word, n) || choices[i].word[n])
continue;
if (!choices[i].rest && *rest)
{
command_reply_error(cmd, family, "nothing after %s",
choices[i].word);
return false;
}
{
bool ret;
command_family_now.family = family;
command_family_now.word = choices[i].word;
command_family_now.name = choices[i].name;
command_family_now.replier = cmd->replier;
cmd->replier = command_family_replier;
ret = command_run(cmd, choices[i].name, rest);
cmd->replier = command_family_now.replier;
/* what answers nothing of its own still answers as the tool */
if (ret && !command_family_now.answered && cmd->structured)
{
char msg[64];
int _len = snprintf(msg, sizeof(msg), "%s %s\n", family,
choices[i].word);
if (_len > 0 && (size_t)_len < sizeof(msg))
cmd->replier(cmd, msg, (size_t)_len);
}
memset(&command_family_now, 0, sizeof(command_family_now));
return ret;
}
}
command_reply_error(cmd, family, "one of %s", values);
return false;
}
#ifdef HAVE_MENU
/* MENU: the request whose action the next menu frame runs, as the
* menu's own input would, answered a frame after that with what the
* menu shows then: a list or a message an action opens is filled in by
* the frame that follows it. */
static struct
{
command_deferred_t deferred;
size_t index;
unsigned from;
unsigned action;
/* menu frames still to run before the answer: the action's, then one */
unsigned frames;
bool pending;
bool has_index;
bool has_from;
bool lost;
/* the list the index was given in: how deep, and its label */
size_t depth;
char list[64];
} command_menu_st;
/* How deep the list shown is, and its label in @s. */
static size_t command_menu_where(char *s, size_t len)
{
menu_list_t *list = menu_state_get_ptr()->entries.list;
file_list_t *stack = list ? MENU_LIST_GET(list, 0) : NULL;
s[0] = '\0';
if (!stack || !stack->size)
return 0;
if (stack->list[stack->size - 1].label)
strlcpy(s, stack->list[stack->size - 1].label, len);
return stack->size;
}
static void command_menu_clean(char *s)
{
for (; *s; s++)
if (*s == '\t' || *s == '\n' || *s == '\r')
*s = ' ';
}
/* The title of the list @i deep in @stack. A playlist's tab is titled
* "Horizontal Menu" there; its playlist's name says which it is. */
static void command_menu_level_title(const file_list_t *stack, size_t i,
char *s, size_t len)
{
struct menu_state *menu_st = menu_state_get_ptr();
const menu_ctx_driver_t *drv = menu_st->driver_ctx;
menu_file_list_cbs_t *cbs =
(menu_file_list_cbs_t*)stack->list[i].actiondata;
s[0] = '\0';
if ( stack->list[i].type == MENU_SETTING_HORIZONTAL_MENU
&& drv && drv->list_get_entry && drv->list_get_selection
&& drv->list_get_size)
{
size_t sel = drv->list_get_selection(menu_st->userdata);
size_t tabs = drv->list_get_size(menu_st->userdata, MENU_LIST_TABS);
struct item_file *item = sel > tabs
? (struct item_file*)drv->list_get_entry(menu_st->userdata,
MENU_LIST_HORIZONTAL, (unsigned)(sel - (tabs + 1)))
: NULL;
if (item && item->path && *item->path)
{
strlcpy(s, path_basename(item->path), len);
path_remove_extension(s);
return;
}
}
if (i + 1 == stack->size)
menu_entries_get_title(s, len);
else if (cbs && cbs->action_get_title)
cbs->action_get_title(stack->list[i].path, stack->list[i].label,
stack->list[i].type, s, len);
}
/* What the menu shows: its title, a page of entries - number, label
* and value - holding the selection, or from @from, and which entry is
* selected of how many. NULL where out of memory; the length in
* @out_len. */
static char *command_menu_answer(bool has_from, unsigned from,
size_t *out_len)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *list = menu_st->entries.list;
file_list_t *buf = list
? MENU_LIST_GET_SELECTION(list, (unsigned)0) : NULL;
size_t count = buf ? buf->size : 0;
size_t sel = menu_st->selection_ptr;
size_t size = 8192 + COMMAND_MENU_PAGE * 560;
size_t _len;
size_t i;
size_t first;
char *out;
menu_entry_t *entry;
if (!(out = (char*)malloc(size)))
return NULL;
if (!(menu_st->flags & MENU_ST_FLAG_ALIVE) || !buf)
{
*out_len = strlcpy(out, "CLOSED\n", size);
return out;
}
if (!(entry = (menu_entry_t*)malloc(sizeof(*entry))))
{
free(out);
return NULL;
}
{
file_list_t *stack = MENU_LIST_GET(list, 0);
char title[256];
title[0] = '\0';
if (stack && stack->size)
command_menu_level_title(stack, stack->size - 1, title,
sizeof(title));
command_menu_clean(title);
_len = (size_t)snprintf(out, size, "MENU %s\n", title);
/* how it got there: the open menu remembers where it was left */
if (stack && stack->size > 1)
{
size_t d;
_len += strlcpy(out + _len, "PATH ", size - _len);
for (d = 0; d < stack->size && d < 16; d++)
{
command_menu_level_title(stack, d, title, sizeof(title));
command_menu_clean(title);
_len += (size_t)snprintf(out + _len, size - _len, "%s%s",
d ? " > " : "", title);
}
_len += strlcpy(out + _len, "\n", size - _len);
}
}
/* what waits on the user over the list: typing, or a message that a
* second a confirms - "Press again to quit" */
if (menu_input_dialog_get_display_kb())
{
char label[sizeof(menu_st->input_dialog_kb_label)];
strlcpy(label, menu_st->input_dialog_kb_label, sizeof(label));
command_menu_clean(label);
_len += (size_t)snprintf(out + _len, size - _len, "KEYBOARD %s\n",
label);
}
else if (menu_st->driver_data && *menu_st->driver_data->menu_state_msg)
{
char msg[sizeof(menu_st->driver_data->menu_state_msg)];
strlcpy(msg, menu_st->driver_data->menu_state_msg, sizeof(msg));
command_menu_clean(msg);
_len += (size_t)snprintf(out + _len, size - _len, "MESSAGE %s\n",
msg);
}
first = has_from ? from : sel - sel % COMMAND_MENU_PAGE;
for (i = first; i < count && i < first + COMMAND_MENU_PAGE; i++)
{
char *label;
char *value;
const char *shown;
MENU_ENTRY_INITIALIZE((*entry));
entry->flags |= MENU_ENTRY_FLAG_PATH_ENABLED
| MENU_ENTRY_FLAG_RICH_LABEL_ENABLED
| MENU_ENTRY_FLAG_VALUE_ENABLED;
menu_entry_get(entry, 0, i, NULL, true);
label = *entry->rich_label ? entry->rich_label : entry->path;
value = entry->enum_idx == MENU_ENUM_LABEL_CHEEVOS_PASSWORD
? entry->password_value : entry->value;
command_menu_clean(label);
command_menu_clean(value);
/* a choice in a list of them has no value, only whether it is the
* one chosen */
shown = !*value && (entry->flags & MENU_ENTRY_FLAG_CHECKED)
? "checked" : value;
_len += (size_t)snprintf(out + _len, size - _len, "%u\t%.255s\t%.255s\n",
(unsigned)i, label, shown);
}
_len += (size_t)snprintf(out + _len, size - _len, "SELECTED %u\nCOUNT %u\n",
(unsigned)sel, (unsigned)count);
if (i < count)
_len += (size_t)snprintf(out + _len, size - _len, "MORE %u\n",
(unsigned)i);
free(entry);
*out_len = _len;
return out;
}
/* The menu's own action for this frame, or the one a MENU request
* asks for in its place: on the entry it names, where it names one. */
unsigned command_menu_action(unsigned action)
{
struct menu_state *menu_st = menu_state_get_ptr();
char where[sizeof(command_menu_st.list)];
if (!command_menu_st.pending || command_menu_st.frames < 2)
return action;
/* a request is input, as a press is: no screensaver comes over it,
* and one up takes the press that ends it */
menu_st->input_last_time_us = menu_st->current_time_us;
if (menu_st->flags & MENU_ST_FLAG_SCREENSAVER_ACTIVE)
{
menu_st->flags &= ~MENU_ST_FLAG_SCREENSAVER_ACTIVE;
if (menu_st->driver_ctx && menu_st->driver_ctx->environ_cb)
menu_st->driver_ctx->environ_cb(MENU_ENVIRON_DISABLE_SCREENSAVER,
NULL, menu_st->userdata);
}
if (command_menu_st.has_index)
{
command_menu_st.has_index = false;
/* the list may have changed since the request checked it - another
* list, or this one shorter: then nothing is done to whichever
* entry took that place */
if ( !menu_st->entries.list
|| command_menu_where(where, sizeof(where))
!= command_menu_st.depth
|| !string_is_equal(where, command_menu_st.list)
|| command_menu_st.index
>= MENU_LIST_GET_SELECTION(menu_st->entries.list, 0)->size)
{
command_menu_st.lost = true;
command_menu_st.action = MENU_ACTION_NOOP;
return action;
}
menu_st->selection_ptr = command_menu_st.index;
if (menu_st->driver_ctx && menu_st->driver_ctx->navigation_set)
menu_st->driver_ctx->navigation_set(menu_st->userdata, true);
}
return command_menu_st.action;
}
/* Before commands are read: a MENU request whose menu closed before
* running it - a hotkey, another client - is answered that it is
* closed, as no menu frame will answer it. */
void command_menu_closed(void)
{
if ( command_menu_st.pending
&& !(menu_state_get_ptr()->flags & MENU_ST_FLAG_ALIVE))
command_menu_iterated();
}
/* After a menu frame: answers the MENU request it ran. */
void command_menu_iterated(void)
{
char *out;
size_t _len = 0;
if (!command_menu_st.pending)
return;
if ( --command_menu_st.frames
&& (menu_state_get_ptr()->flags & MENU_ST_FLAG_ALIVE))
return;
command_menu_st.pending = false;
if (command_menu_st.lost)
{
command_menu_st.lost = false;
command_deferred_send(&command_menu_st.deferred,
"MENU ERROR the list changed before the entry could be "
"reached\n", STRLEN_CONST("MENU ERROR the list changed before "
"the entry could be reached\n"), true);
return;
}
if ((out = command_menu_answer(command_menu_st.has_from,
command_menu_st.from, &_len)))
command_deferred_send(&command_menu_st.deferred, out, _len, false);
else
command_deferred_send(&command_menu_st.deferred,
"MENU ERROR out of memory\n",
STRLEN_CONST("MENU ERROR out of memory\n"), true);
free(out);
}
#endif
/* MENU [index] [from ] */
bool command_menu(command_t *cmd, const char *arg)
{
#ifdef HAVE_MENU
static const struct
{
const char *word;
unsigned action;
bool index;
} actions[] = {
{ "show", MENU_ACTION_NOOP, false },
{ "open", MENU_ACTION_NOOP, false },
{ "close", MENU_ACTION_NOOP, false },
{ "select", MENU_ACTION_NOOP, true },
{ "up", MENU_ACTION_UP, false },
{ "down", MENU_ACTION_DOWN, false },
{ "left", MENU_ACTION_LEFT, true },
{ "right", MENU_ACTION_RIGHT, true },
{ "a", MENU_ACTION_OK, true },
{ "b", MENU_ACTION_CANCEL, false },
{ "type", MENU_ACTION_NOOP, false }
};
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *list = menu_st->entries.list;
file_list_t *buf = list
? MENU_LIST_GET_SELECTION(list, (unsigned)0) : NULL;
bool alive = (menu_st->flags & MENU_ST_FLAG_ALIVE) && buf;
const char *p = arg ? arg : "";
char *end;
unsigned long index = 0;
unsigned long from = 0;
bool has_index = false;
bool has_from = false;
size_t n;
size_t i;
while (*p == ' ')
p++;
for (n = 0; p[n] && p[n] != ' '; n++)
;
for (i = 0; i < ARRAY_SIZE(actions); i++)
if (!strncmp(p, actions[i].word, n) && !actions[i].word[n])
break;
if (!n || i == ARRAY_SIZE(actions))
{
command_reply_error(cmd, "MENU", "one of %s", COMMAND_MENU_ACTIONS);
return false;
}
for (p += n; *p == ' '; p++)
;
/* the rest of the line is the text, spaces and all */
if (string_is_equal(actions[i].word, "type"))
{
if (!alive || !menu_input_dialog_get_display_kb())
{
command_reply_error(cmd, "MENU", "no keyboard is open: a on an "
"entry that asks for text opens one");
return false;
}
for (n = 0; p[n]; n++)
if ((unsigned char)p[n] < 0x20 || p[n] == 0x7f)
{
command_reply_error(cmd, "MENU", "type takes one line of text, "
"with no tab or control character");
return false;
}
input_driver_keyboard_line_set(p, n);
input_keyboard_event(true, '\n', '\n', 0, RETRO_DEVICE_KEYBOARD);
input_keyboard_event(false, '\n', 0, 0, RETRO_DEVICE_KEYBOARD);
p += n;
}
else if (*p >= '0' && *p <= '9')
{
index = strtoul(p, &end, 10);
has_index = true;
for (p = end; *p == ' '; p++)
;
}
if (!strncmp(p, "from ", 5))
{
for (p += 5; *p == ' '; p++)
;
if (*p < '0' || *p > '9')
{
command_reply_error(cmd, "MENU", "an entry number after from");
return false;
}
from = strtoul(p, &end, 10);
has_from = true;
for (p = end; *p == ' '; p++)
;
}
if (*p)
{
command_reply_error(cmd, "MENU", "%s [index] [from ], not %s",
actions[i].word, p);
return false;
}
if (has_index && !actions[i].index)
{
command_reply_error(cmd, "MENU", "%s takes no index", actions[i].word);
return false;
}
if (!has_index && string_is_equal(actions[i].word, "select"))
{
command_reply_error(cmd, "MENU", "select needs an index");
return false;
}
if (string_is_equal(actions[i].word, "close"))
{
char *out;
size_t _len = 0;
if (menu_st->flags & MENU_ST_FLAG_ALIVE)
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
if (menu_st->flags & MENU_ST_FLAG_ALIVE)
{
command_reply_error(cmd, "MENU", "the menu stays open with no "
"content running");
return false;
}
if (!(out = command_menu_answer(false, 0, &_len)))
return false;
cmd->replier(cmd, out, _len);
free(out);
return true;
}
if (string_is_equal(actions[i].word, "open"))
{
if (!(menu_st->flags & MENU_ST_FLAG_ALIVE))
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
if (!(menu_st->flags & MENU_ST_FLAG_ALIVE))
{
command_reply_error(cmd, "MENU", "the menu cannot open now");
return false;
}
}
else if (!alive && !string_is_equal(actions[i].word, "show"))
{
command_reply_error(cmd, "MENU", "the menu is closed: MENU open "
"opens it");
return false;
}
else if (has_index && index >= buf->size)
{
command_reply_error(cmd, "MENU", "no entry %lu: the list has %u",
index, (unsigned)buf->size);
return false;
}
/* what nothing changes is answered now */
if (string_is_equal(actions[i].word, "show"))
{
char *out;
size_t _len = 0;
if (!(out = command_menu_answer(has_from, (unsigned)from, &_len)))
return false;
cmd->replier(cmd, out, _len);
free(out);
return true;
}
if (command_menu_st.pending)
command_deferred_send(&command_menu_st.deferred,
"MENU ERROR replaced by another MENU request\n",
STRLEN_CONST("MENU ERROR replaced by another MENU request\n"),
true);
command_menu_st.action = actions[i].action;
command_menu_st.index = (size_t)index;
command_menu_st.has_index = has_index;
command_menu_st.from = (unsigned)from;
command_menu_st.has_from = has_from;
command_menu_st.depth = command_menu_where(command_menu_st.list,
sizeof(command_menu_st.list));
command_menu_st.lost = false;
command_menu_st.frames = 2;
command_menu_st.pending = true;
command_deferred_take(&command_menu_st.deferred, cmd);
return true;
#else
command_reply_error(cmd, "MENU", "this build has no menu");
return false;
#endif
}
bool command_disk(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "eject", "DISK_EJECT_TOGGLE", false },
{ "next", "DISK_NEXT", false },
{ "prev", "DISK_PREV", false }
};
/* the hotkeys are pressed regardless; the tool says when they can
* do nothing */
if (!disk_control_enabled(&runloop_state_get_ptr()->system.disk_control))
{
command_reply_error(cmd, "DISK", "this core has no disk control");
return false;
}
return command_run_choice(cmd, "DISK", choices, ARRAY_SIZE(choices),
COMMAND_DISK_ACTIONS, arg);
}
bool command_reinit(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "video", "VIDEO_REINIT", false },
{ "audio", "AUDIO_REINIT", false },
{ "all", "DRIVERS_REINIT", false }
};
return command_run_choice(cmd, "REINIT", choices, ARRAY_SIZE(choices),
COMMAND_REINIT_DRIVERS, arg);
}
bool command_state(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "save", "SAVE_STATE_SLOT", true },
{ "load", "LOAD_STATE_SLOT", true },
{ "checkpoint", "CHECKPOINT_STATE", false },
{ "rollback", "ROLLBACK_STATE", false }
};
return command_run_choice(cmd, "STATE", choices, ARRAY_SIZE(choices),
COMMAND_STATE_ACTIONS, arg);
}
bool command_core(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "load", "LOAD_CORE", true },
{ "start", "START_CORE", false },
{ "unload", "UNLOAD_CORE", false }
};
return command_run_choice(cmd, "CORE", choices, ARRAY_SIZE(choices),
COMMAND_CORE_ACTIONS, arg);
}
bool command_replay(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "play", "PLAY_REPLAY_SLOT", true },
{ "seek", "SEEK_REPLAY", true },
{ "record", "RECORD_REPLAY", false },
{ "halt", "HALT_REPLAY", false },
{ "checkpoint", "SAVE_REPLAY_CHECKPOINT", false }
};
return command_run_choice(cmd, "REPLAY", choices, ARRAY_SIZE(choices),
COMMAND_REPLAY_ACTIONS, arg);
}
bool command_input(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "set", "SET_INPUT", true },
{ "release", "RELEASE_INPUT", true },
{ "show", "GET_INPUT", true }
};
return command_run_choice(cmd, "INPUT", choices, ARRAY_SIZE(choices),
COMMAND_INPUT_ACTIONS, arg);
}
bool command_option(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "show", "GET_OPTION", true },
{ "set", "SET_OPTION", true }
};
return command_run_choice(cmd, "OPTION", choices, ARRAY_SIZE(choices),
COMMAND_OPTION_ACTIONS, arg);
}
bool command_core_option(command_t *cmd, const char *arg)
{
static const struct cmd_choice choices[] = {
{ "show", "GET_CORE_OPTION", true },
{ "set", "SET_CORE_OPTION", true }
};
return command_run_choice(cmd, "CORE_OPTION", choices,
ARRAY_SIZE(choices), COMMAND_OPTION_ACTIONS, arg);
}
bool command_interfaces_held(void)
{
return runloop_is_content_switching();
}
const struct cmd_action_map *command_action_list(size_t *count)
{
*count = ARRAY_SIZE(action_map);
return action_map;
}
const struct cmd_map *command_hotkey_list(size_t *count)
{
*count = ARRAY_SIZE(map);
return map;
}
#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 = (int)socket(tmp_info->ai_family,
tmp_info->ai_socktype, tmp_info->ai_protocol);
if (fd < 0)
continue;
if (sendto(fd, msg, (int)len, 0, tmp_info->ai_addr, (socklen_t)tmp_info->ai_addrlen) ==
(ssize_t)len)
ret = true;
socket_close(fd);
} while ((tmp_info = tmp_info->ai_next));
freeaddrinfo_retro(addr);
return ret;
}
/* Whether @s, after a ";", can be a host or a port rather than a STEPS
* step: no space, no "|" (a list's "|on"), and not "checkpoint". Empty
* is the default, as in "CMD;;PORT". */
static bool command_network_host_like(const char *s)
{
return !strchr(s, ' ') && !strchr(s, '|')
&& !string_is_equal(s, "checkpoint");
}
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);
/* ";host;port" ends it, the two read from the end and only where they
* can be no STEPS step: a list separates its steps with ";" too */
cmd = buf;
if ((ptr = strrchr(buf, ';')) && command_network_host_like(ptr + 1))
{
char *prev;
*ptr = '\0';
host = ptr + 1;
if ((prev = strrchr(buf, ';')) && command_network_host_like(prev + 1))
{
*prev = '\0';
port_str = host;
host = prev + 1;
}
}
if (!cmd || !*cmd)
return false;
if (!host || !*host)
{
#ifdef _WIN32
host = "127.0.0.1";
#else
host = "localhost";
#endif
}
if (port_str && *port_str)
{
char *end;
unsigned long val = strtoul(port_str, &end, 0);
if (*end || !val || val > 0xFFFF)
{
RARCH_ERR("[NetCMD] Invalid port \"%s\".\n", port_str);
return false;
}
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;
}
/* A whole number and nothing after it but spaces, into @out. "3xyz" is
* not one: it used to come out as 3, and a save then overwrote that
* slot on an argument its sender had got wrong. */
static bool command_arg_uint(const char *arg, unsigned *out)
{
char *end = NULL;
unsigned long v;
while (arg && *arg == ' ')
arg++;
if (!arg || *arg < '0' || *arg > '9')
return false;
errno = 0;
v = strtoul(arg, &end, 10);
while (*end == ' ')
end++;
if (*end)
return false;
*out = (unsigned)v;
/* range-checked before narrowing, so 4294967297 is not slot 1, and
* where long is 32 bits strtoul clamps it and says so */
return errno != ERANGE && (unsigned long)*out == v;
}
/* A state or replay slot: past INT_MAX one turns negative, which is the
* auto slot's file. */
static bool command_arg_slot(const char *arg, unsigned *out)
{
return command_arg_uint(arg, out) && *out <= INT_MAX;
}
static bool command_load_state_start(struct command_reply *r)
{
return content_load_state_notify(r->path, false, false,
command_reply_done, r);
}
/* Serializing re-enters the core, so this must not run from a command's
* own poll. */
static bool command_save_state_start(struct command_reply *r)
{
if (!content_save_state_notify(r->path, true, command_reply_done, r))
{
/* on the heap: this runs from a task, whose thread may have a
* small stack */
const char *what = msg_hash_to_str(MSG_FAILED_TO_SAVE_STATE_TO);
size_t len = strlen(what) + strlen(r->path) + 8;
char *msg = (char*)malloc(len);
if (msg)
snprintf(msg, len, "%s \"%s\".", what, r->path);
command_reply_done(NULL, NULL, r, msg ? msg : what);
free(msg);
}
return true;
}
/* LOAD_STATE_SLOT and SAVE_STATE_SLOT, answered once their task is
* through. */
static bool command_state_slot(command_t *cmd, const char *arg, bool load)
{
const char *name = load ? "LOAD_STATE_SLOT" : "SAVE_STATE_SLOT";
unsigned int slot = 0;
bool savestates_enabled = core_info_current_supports_savestate();
struct command_reply *r;
if (!command_arg_slot(arg, &slot))
{
command_reply_error(cmd, name, "a slot number, not %s",
(arg && *arg) ? arg : "nothing");
return false;
}
if ((r = command_reply_new(name)))
{
runloop_get_savestate_path(r->path, sizeof(r->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 (load && !savestates_enabled)
savestates_enabled = path_is_valid(r->path);
if (savestates_enabled)
{
snprintf(r->ok, sizeof(r->ok), "%s %u", name, slot);
r->state_loaded = load;
r->start = load ? command_load_state_start
: command_save_state_start;
return command_reply_push(cmd, r);
}
free(r);
command_reply_error(cmd, name,
load ? "no state in slot %u, and this core cannot restore one"
: "this core cannot save a state to slot %u right now",
slot);
return false;
}
command_reply_error(cmd, name, "out of memory");
return false;
}
/* The state STATE checkpoint keeps, in memory rather than a slot's
* file, for STATE rollback; dropped when the content closes. */
static struct
{
void *data;
size_t size;
} command_checkpoint;
void command_checkpoint_clear(void)
{
free(command_checkpoint.data);
command_checkpoint.data = NULL;
command_checkpoint.size = 0;
}
/* Keeps the core's state as the checkpoint. Serializing re-enters the
* core, so only with the core off the stack. */
static bool command_checkpoint_take(void)
{
retro_ctx_serialize_info_t info;
size_t len = core_serialize_size();
void *buf = len ? malloc(len) : NULL;
info.data_const = NULL;
info.data = buf;
info.size = len;
if (!buf || !core_serialize(&info))
{
free(buf);
return false;
}
free(command_checkpoint.data);
command_checkpoint.data = buf;
command_checkpoint.size = len;
return true;
}
/* Serializing re-enters the core, so this runs from the reply's main
* thread task, as a save to a slot does. */
static bool command_checkpoint_start(struct command_reply *r)
{
const char *error = NULL;
if (r->state_loaded)
{
bool ramped;
if (!command_checkpoint.data)
error = "no checkpoint to roll back to; STATE checkpoint keeps one";
/* as a load from a slot is refused: the recording would go on
* from a state it never saw */
else if (input_driver_replay_recording())
error = "a replay is recording, which a rollback would break";
else
{
/* through the loader of states without RetroArch's wrapper,
* which resets the achievements and stops a replay playing,
* as a load does */
ramped = audio_driver_jump_fade_begin();
if (!content_deserialize_state(command_checkpoint.data,
command_checkpoint.size))
error = "the core could not restore the checkpoint";
audio_driver_jump_fade_end(ramped);
}
}
else if (!command_checkpoint_take())
error = "this core cannot save a state right now";
command_reply_done(NULL, NULL, r, error);
return true;
}
static bool command_checkpoint_state(command_t *cmd, const char *arg,
bool rollback)
{
const char *name = rollback ? "ROLLBACK_STATE" : "CHECKPOINT_STATE";
struct command_reply *r;
while (arg && *arg == ' ')
arg++;
if (arg && *arg)
{
command_reply_error(cmd, name, "nothing after it");
return false;
}
if (!core_info_current_supports_savestate())
{
command_reply_error(cmd, name, "this core cannot save a state");
return false;
}
#ifdef HAVE_CHEEVOS
if (rcheevos_hardcore_active())
{
command_reply_error(cmd, name,
"achievements hardcore mode forbids states");
return false;
}
#endif
if (!(r = command_reply_new(name)))
{
command_reply_error(cmd, name, "out of memory");
return false;
}
r->state_loaded = rollback;
r->start = command_checkpoint_start;
return command_reply_push(cmd, r);
}
bool command_checkpoint_save(command_t *cmd, const char *arg)
{
return command_checkpoint_state(cmd, arg, false);
}
bool command_checkpoint_rollback(command_t *cmd, const char *arg)
{
return command_checkpoint_state(cmd, arg, true);
}
/* RESET [on|off]: from the reply's main thread task, since a reset
* re-enters the core; with on, paused after it, as LOAD_CONTENT ...
* paused is, so nothing runs before the first step. */
static bool command_reset_start(struct command_reply *r)
{
command_event(CMD_EVENT_RESET, NULL);
command_reply_done(NULL, NULL, r, NULL);
return true;
}
bool command_reset(command_t *cmd, const char *arg)
{
struct command_reply *r;
bool pause = false;
while (arg && *arg == ' ')
arg++;
if (arg && *arg)
{
if (string_is_equal(arg, "on"))
pause = true;
else if (!string_is_equal(arg, "off"))
{
command_reply_error(cmd, "RESET", "on or off, not %s", arg);
return false;
}
}
if (!(runloop_get_flags() & RUNLOOP_FLAG_CORE_RUNNING))
{
command_reply_error(cmd, "RESET", "no content is running");
return false;
}
if (!(r = command_reply_new("RESET")))
{
command_reply_error(cmd, "RESET", "out of memory");
return false;
}
r->pause = pause;
r->reset = true;
r->start = command_reset_start;
return command_reply_push(cmd, r);
}
bool command_load_state_slot(command_t *cmd, const char *arg)
{
return command_state_slot(cmd, arg, true);
}
bool command_save_state_slot(command_t *cmd, const char *arg)
{
return command_state_slot(cmd, arg, false);
}
bool command_play_replay_slot(command_t *cmd, const char *arg)
{
#ifdef HAVE_BSV_MOVIE
unsigned int slot = 0;
struct command_reply *r = NULL;
if (!command_arg_slot(arg, &slot))
{
command_reply_error(cmd, "PLAY_REPLAY_SLOT", "a slot number, not %s",
(arg && *arg) ? arg : "nothing");
return false;
}
if ( core_info_current_supports_savestate()
&& (r = command_reply_new("PLAY_REPLAY_SLOT")))
{
runloop_get_replay_path(r->path, sizeof(r->path), slot);
r->state_loaded = true;
/* answered from the movie task's callback, on a later frame */
if (movie_start_playback_notify(input_state_get_ptr(), r->path,
command_reply_done, r))
{
command_deferred_take(&r->deferred, cmd);
return true;
}
free(r);
command_reply_error(cmd, "PLAY_REPLAY_SLOT",
"no replay in slot %u", slot);
return false;
}
command_reply_error(cmd, "PLAY_REPLAY_SLOT",
"this core cannot play a replay");
return false;
#else
command_reply_error(cmd, "PLAY_REPLAY_SLOT",
"this build has no replay support");
return false;
#endif
}
bool command_seek_replay(command_t *cmd, const char *arg)
{
#ifdef HAVE_BSV_MOVIE
char reply[32];
char *endptr = NULL;
size_t _len;
int64_t frame = arg ? (int64_t)strtoll(arg, &endptr, 10) : 0;
input_driver_state_t *input_st = input_state_get_ptr();
struct command_reply *r;
/* strtoll always writes a valid pointer, so an empty or non-numeric
* argument shows up as no characters consumed; anything but spaces
* after the number means it was not one. */
if (arg && endptr != arg)
while (*endptr == ' ')
endptr++;
if (!arg || endptr == arg || *endptr)
{
command_reply_error(cmd, "SEEK_REPLAY", "a frame number, not %s",
(arg && *arg) ? arg : "nothing");
return false;
}
if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_PLAYBACK | BSV_FLAG_MOVIE_RECORDING)))
{
command_reply_error(cmd, "SEEK_REPLAY",
"no replay is playing or recording");
return false;
}
#ifdef HAVE_CHEEVOS
if (rcheevos_hardcore_active())
{
command_reply_error(cmd, "SEEK_REPLAY",
"achievements hardcore mode forbids seeking");
return false;
}
#endif
if (!movie_seek_to_frame(input_st, frame))
{
command_reply_error(cmd, "SEEK_REPLAY",
"the replay could not be seeked to frame %" PRId64, frame);
return false;
}
/* A structured request is answered once the seek has run, on a
* later frame; a line-based one at once, as it always was. */
if (cmd->structured && (r = command_reply_new("SEEK_REPLAY")))
{
snprintf(r->ok, sizeof(r->ok), "OK %" PRId64,
input_st->bsv_movie_state.seek_target_frame);
task_notify_set(&input_st->bsv_movie_op, command_reply_done, r);
command_deferred_take(&r->deferred, cmd);
return true;
}
_len = (size_t)snprintf(reply, sizeof(reply), "OK %" PRId64 "\n",
input_st->bsv_movie_state.seek_target_frame);
cmd->replier(cmd, reply, _len);
return true;
#else
command_reply_error(cmd, "SEEK_REPLAY",
"this build has no replay support");
return false;
#endif
}
bool command_save_savefiles(command_t *cmd, const char* arg)
{
/* In the future, this should probably send each saved file path
to the replier. */
if (!command_event(CMD_EVENT_SAVE_FILES, NULL))
{
command_reply_error(cmd, "SAVE_FILES",
"nothing to save: no content is loaded, or the core has no "
"save files");
return false;
}
cmd->replier(cmd, "OK\n", 3);
return true;
}
bool command_load_savefiles(command_t *cmd, const char* arg)
{
if (!command_event(CMD_EVENT_LOAD_FILES, NULL))
{
command_reply_error(cmd, "LOAD_FILES",
"nothing to load: no content is loaded, or the core has no "
"save files");
return false;
}
cmd->replier(cmd, "OK\n", 3);
return true;
}
/* Largest byte count READ_CORE_RAM / READ_CORE_MEMORY will serve. The
* reply carries 3 characters per byte and goes out as one UDP datagram,
* so anything much bigger could not be delivered anyway; the cap also
* keeps `nbytes * 3` far from wrapping in unsigned int, which a request
* of ~1431655766 bytes did, allocating a few bytes and writing 4 GiB. */
#define COMMAND_READ_NBYTES_MAX 16384u
/* A system or achievement address in hexadecimal, range-checked before
* it is narrowed, so 100001001 is refused rather than read as 1001. */
static bool command_parse_address(const char *s, unsigned *address,
char **end)
{
unsigned long num;
while (s && *s == ' ')
s++;
if (!s || (unsigned char)*s <= ' ' || *s == '-' || *s == '+')
return false;
errno = 0;
num = strtoul(s, end, 16);
*address = (unsigned)num;
return *end != s && errno != ERANGE && (unsigned long)*address == num;
}
/* The next of a run of hexadecimal bytes separated by spaces: 1 with
* it in @out, 0 at the end, -1 at anything that is not a byte. A
* write checks every byte before it makes the first. */
static int command_next_byte(const char **s, uint8_t *out)
{
char *end = NULL;
unsigned long num;
while (**s == ' ')
(*s)++;
if (!**s)
return 0;
if (**s == '-' || **s == '+')
return -1;
num = strtoul(*s, &end, 16);
if (end == *s || num > 0xff || (*end && *end != ' '))
return -1;
*s = end;
*out = (uint8_t)num;
return 1;
}
static bool command_bytes_valid(const char *s)
{
uint8_t b;
int r;
while ((r = command_next_byte(&s, &b)) > 0)
;
return r == 0;
}
/* @n bytes of @data as " XX" apiece; gives back the end of what it
* wrote. */
static char *command_hex_bytes(char *at, const uint8_t *data, size_t n)
{
static const char hex[] = "0123456789ABCDEF";
size_t i;
for (i = 0; i < n; i++)
{
*at++ = ' ';
*at++ = hex[data[i] >> 4];
*at++ = hex[data[i] & 0x0f];
}
return at;
}
#if defined(HAVE_CHEEVOS)
bool command_read_ram(command_t *cmd, const char *arg)
{
unsigned long num = 0;
unsigned int nbytes = 0;
unsigned int addr = 0;
char *end = NULL;
if (command_parse_address(arg, &addr, &end) && *end == ' ')
num = strtoul(end + 1, NULL, 10);
nbytes = num <= COMMAND_READ_NBYTES_MAX ? (unsigned)num : 0;
if (!nbytes)
{
command_reply_error(cmd, "READ_CORE_RAM",
"an achievement address in hexadecimal, a space, and a byte "
"count from 1 to %u", COMMAND_READ_NBYTES_MAX);
return false;
}
{
size_t _len = 0;
char *reply_at = NULL;
unsigned int avail = 0;
const uint8_t *data = rcheevos_patch_address_avail(addr, &avail);
unsigned int alloc_size;
char *reply;
/* Never read past the end of the region the address lives in. */
if (data && nbytes > avail)
nbytes = avail;
/* We allocate more than needed, saving 20 bytes is not really relevant */
alloc_size = 40 + nbytes * 3;
reply = (char*)malloc(alloc_size);
if (!reply)
{
command_reply_error(cmd, "READ_CORE_RAM", "out of memory");
return false;
}
reply[0] = '\0';
reply_at = reply + snprintf(
reply, alloc_size - 1, "READ_CORE_RAM" " %x", addr);
if (data && nbytes > 0)
{
char *end = command_hex_bytes(reply_at, data, nbytes);
*end++ = '\n';
_len = (size_t)(end - reply);
}
else
{
/* no achievement memory at this address; " -1" is what line
* clients have always read */
strlcpy_lit(reply_at, " -1\n", alloc_size - (size_t)(reply_at - reply));
_len = reply_at + STRLEN_CONST(" -1\n") - reply;
cmd->error = true;
}
cmd->replier(cmd, reply, _len);
free(reply);
}
return !cmd->error;
}
bool command_write_ram(command_t *cmd, const char *arg)
{
unsigned int addr = 0;
unsigned int avail = 0;
char *end = NULL;
uint8_t *data = NULL;
uint8_t more;
const uint8_t *start;
if (!command_parse_address(arg, &addr, &end) || !command_bytes_valid(end))
{
command_reply_error(cmd, "WRITE_CORE_RAM", "an achievement address "
"in hexadecimal, then bytes from 00 to ff separated by "
"spaces");
return false;
}
arg = end;
data = (uint8_t *)rcheevos_patch_address_avail(addr, &avail);
start = data;
if (!data)
{
command_reply_error(cmd, "WRITE_CORE_RAM",
"no achievement memory at %x; a game with achievements must "
"be running", addr);
return false;
}
if (rcheevos_hardcore_active())
{
RARCH_LOG("[Command] Achievements hardcore mode disabled by WRITE_CORE_RAM.\n");
rcheevos_pause_hardcore();
}
/* Stop at the end of the region: the payload length is whatever the
* sender put in the datagram. */
while (avail && command_next_byte(&arg, data) > 0)
{
data++;
avail--;
}
/* read into a byte of its own: @data is past the region here */
if (command_next_byte(&arg, &more) > 0)
RARCH_WARN("[Command] WRITE_CORE_RAM at %x reached the end of the "
"memory region; remainder of the payload ignored.\n", addr);
{
char reply[64];
size_t _len = (size_t)snprintf(reply, sizeof(reply),
"WRITE_CORE_RAM %x %u\n", addr, (unsigned)(data - start));
cmd->replier(cmd, reply, _len);
}
return true;
}
#endif
/* ---- Queries for clients that browse and launch content --------- */
/* A reply larger than one UDP datagram cannot go back over the network
* command port; lists stop short of it and say where to go on. */
#define COMMAND_LIST_REPLY_MAX 60000
#define COMMAND_PLAYLIST_PAGE 200
/* ------------------------------------------------------------------ */
/* Options: what GET_OPTION reports and SET_OPTION writes */
/* Which option a row of the table below is. Every one has a case in
* command_option_read(), and no default, so that one added here without
* a case is a warning rather than an option that reports nothing. */
enum cmd_option_id
{
/* on or off */
CMD_OPT_PAUSE = 0,
CMD_OPT_FAST_FORWARD,
CMD_OPT_SLOW_MOTION,
CMD_OPT_MUTE,
CMD_OPT_FPS,
CMD_OPT_STATISTICS,
CMD_OPT_FULLSCREEN,
CMD_OPT_SHADER,
CMD_OPT_VIDEO_FILTER,
CMD_OPT_REWIND,
CMD_OPT_RUNAHEAD,
CMD_OPT_PREEMPT,
CMD_OPT_VRR_RUNLOOP,
CMD_OPT_TURBO_FIRE,
CMD_OPT_GRAB_MOUSE,
CMD_OPT_GAME_FOCUS,
CMD_OPT_RECORDING,
CMD_OPT_STREAMING,
CMD_OPT_CHEEVOS_HARDCORE,
CMD_OPT_CHEEVOS_ENABLE,
CMD_OPT_LOADING,
/* a value they report */
CMD_OPT_FRAME_COUNT,
CMD_OPT_STATE_SLOT,
CMD_OPT_REPLAY_SLOT,
CMD_OPT_ACTIVE_REPLAY,
CMD_OPT_SYSTEM,
CMD_OPT_CONTENT,
CMD_OPT_DIR_SAVEFILE,
CMD_OPT_DIR_SAVESTATE,
CMD_OPT_DIR_RUNTIME_LOG,
CMD_OPT_DIR_LOG,
CMD_OPT_DIR_CACHE,
CMD_OPT_DIR_SYSTEM,
CMD_OPT_NICKNAME
};
/* An option GET_OPTION reports and SET_OPTION writes.
*
* @alias is the name GET_CONFIG_PARAM knew the option by, kept so that
* interface answers what it always did; NULL where there was none.
*
* SET_OPTION is asked for a state, not a change, and writes only when
* the option is not already in it, so sending the same value twice is
* one change and then nothing. That is why @event, the single event the
* hotkey toggles the option with, is enough for most of them; the few
* the frontend does differently are @settable with no event, and
* command_option_set() has them by @id. */
struct cmd_option_map
{
const char *str;
const char *alias;
enum cmd_option_id id;
bool settable;
enum event_command event;
const char *desc;
};
/* Whether @o is on, and its value into @s - @yes or @no for one that is
* on or off, else what it reports - unless @s is NULL.
*
* Every option is read where its state lives rather than from the
* setting behind it: a setting says what was configured, and this says
* what is true now. An option a build does not have never reaches here,
* having no row in the table, but its case is still compiled: what the
* state would be read from may not exist. */
static bool command_option_read(const struct cmd_option_map *o,
char *s, size_t len, const char *yes, const char *no)
{
settings_t *settings = config_get_ptr();
const char *text = NULL;
bool on = false;
char num[64];
switch (o->id)
{
case CMD_OPT_PAUSE:
on = (runloop_get_flags() & RUNLOOP_FLAG_PAUSED) != 0;
break;
case CMD_OPT_FAST_FORWARD:
on = (runloop_get_flags() & RUNLOOP_FLAG_FASTMOTION) != 0;
break;
case CMD_OPT_SLOW_MOTION:
on = (runloop_get_flags() & RUNLOOP_FLAG_SLOWMOTION) != 0;
break;
case CMD_OPT_MUTE:
on = audio_state_get_ptr()->mute_enable;
break;
case CMD_OPT_FPS:
on = settings->bools.video_fps_show;
break;
case CMD_OPT_STATISTICS:
on = settings->bools.video_statistics_show;
break;
case CMD_OPT_FULLSCREEN:
on = settings->bools.video_fullscreen;
break;
case CMD_OPT_SHADER:
#if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL)
on = settings->bools.video_shader_enable;
#endif
break;
case CMD_OPT_VIDEO_FILTER:
#ifdef HAVE_VIDEO_FILTER
on = settings->bools.video_filter_enable;
#endif
break;
case CMD_OPT_REWIND:
#ifdef HAVE_REWIND
on = settings->bools.rewind_enable;
#endif
break;
case CMD_OPT_RUNAHEAD:
#ifdef HAVE_RUNAHEAD
on = settings->bools.run_ahead_enabled;
#endif
break;
case CMD_OPT_PREEMPT:
#ifdef HAVE_RUNAHEAD
on = settings->bools.preemptive_frames_enable;
#endif
break;
case CMD_OPT_VRR_RUNLOOP:
on = settings->bools.vrr_runloop_enable;
break;
case CMD_OPT_TURBO_FIRE:
on = settings->bools.input_turbo_enable;
break;
case CMD_OPT_GRAB_MOUSE:
on = input_driver_mouse_grabbed();
break;
case CMD_OPT_GAME_FOCUS:
on = input_driver_game_focus_enabled();
break;
/* Both are the recorder running; the stream is the one that has
* somewhere to send it. */
case CMD_OPT_RECORDING:
case CMD_OPT_STREAMING:
{
const recording_state_t *rec_st = recording_state_get_ptr();
on = rec_st && rec_st->enable
&& (rec_st->streaming_enable == (o->id == CMD_OPT_STREAMING));
}
break;
case CMD_OPT_CHEEVOS_HARDCORE:
#ifdef HAVE_CHEEVOS
on = rcheevos_hardcore_active();
#endif
break;
case CMD_OPT_CHEEVOS_ENABLE:
#ifdef HAVE_CHEEVOS
on = settings->bools.cheevos_enable;
#endif
break;
case CMD_OPT_LOADING:
on = command_interfaces_held();
break;
case CMD_OPT_FRAME_COUNT:
snprintf(num, sizeof(num), "%" PRIu64,
video_driver_get_frame_count());
text = num;
break;
case CMD_OPT_STATE_SLOT:
snprintf(num, sizeof(num), "%d", settings->ints.state_slot);
text = num;
break;
case CMD_OPT_REPLAY_SLOT:
snprintf(num, sizeof(num), "%d", settings->ints.replay_slot);
text = num;
break;
/* the replay's identifier, its state flags and the frame it is
* on, as GET_CONFIG_PARAM has always given them */
case CMD_OPT_ACTIVE_REPLAY:
text = "0 0 0";
#ifdef HAVE_BSV_MOVIE
{
input_driver_state_t *input_st = input_state_get_ptr();
if (input_st->bsv_movie_state_handle)
{
bsv_movie_t *movie = input_st->bsv_movie_state_handle;
snprintf(num, sizeof(num), "%lld %u %lld",
(long long)movie->identifier,
input_st->bsv_movie_state.flags,
(long long)movie->frame_counter);
text = num;
}
}
#endif
break;
case CMD_OPT_SYSTEM:
{
core_info_t *core_info = NULL;
runloop_state_t *runloop_st = runloop_state_get_ptr();
core_info_get_current_core(&core_info);
if (core_info && core_info->system_id)
text = core_info->system_id;
else
text = runloop_st->system.info.library_name;
}
break;
case CMD_OPT_CONTENT:
text = path_get(RARCH_PATH_BASENAME);
break;
case CMD_OPT_DIR_SAVEFILE:
text = dir_get_ptr(RARCH_DIR_SAVEFILE);
break;
case CMD_OPT_DIR_SAVESTATE:
text = dir_get_ptr(RARCH_DIR_SAVESTATE);
break;
case CMD_OPT_DIR_RUNTIME_LOG:
text = settings->paths.directory_runtime_log;
break;
case CMD_OPT_DIR_LOG:
text = settings->paths.log_dir;
break;
case CMD_OPT_DIR_CACHE:
text = settings->paths.directory_cache;
break;
case CMD_OPT_DIR_SYSTEM:
text = settings->paths.directory_system;
break;
case CMD_OPT_NICKNAME:
text = settings->paths.username;
break;
}
if (s)
strlcpy(s, text ? text : (on ? yes : no), len);
return on;
}
/* Puts @o in state @on, which it is not already in. */
static bool command_option_set(const struct cmd_option_map *o,
command_t *cmd, bool on)
{
switch (o->id)
{
case CMD_OPT_PAUSE:
command_event(on ? CMD_EVENT_PAUSE : CMD_EVENT_UNPAUSE, NULL);
break;
case CMD_OPT_RECORDING:
case CMD_OPT_STREAMING:
if (on && recording_state_get_ptr()->enable)
{
command_reply_error(cmd, "SET_OPTION",
"stop the active recording or stream before starting %s",
o->str);
return false;
}
if (o->id == CMD_OPT_STREAMING)
command_event(CMD_EVENT_STREAMING_TOGGLE, NULL);
else
command_event(on ? CMD_EVENT_RECORD_INIT
: CMD_EVENT_RECORD_DEINIT, NULL);
break;
case CMD_OPT_GAME_FOCUS:
{
enum input_game_focus_cmd_type type = on
? GAME_FOCUS_CMD_ON : GAME_FOCUS_CMD_OFF;
command_event(CMD_EVENT_GAME_FOCUS_TOGGLE, &type);
}
break;
case CMD_OPT_FAST_FORWARD:
return runloop_set_fastmotion(on);
case CMD_OPT_SLOW_MOTION:
return runloop_set_slowmotion(on);
default:
command_event(o->event, NULL);
break;
}
/* An event that cannot act now - pausing with no core running, the
* video filter likewise - does nothing and reports success all the
* same, so what it reached is read back. */
return command_option_read(o, NULL, 0, NULL, NULL) == on;
}
static const struct cmd_option_map option_map[] = {
{ "pause", NULL, CMD_OPT_PAUSE, true, CMD_EVENT_NONE,
"The content is paused." },
{ "fast_forward", NULL, CMD_OPT_FAST_FORWARD, true, CMD_EVENT_NONE,
"Fast-forward is running." },
{ "slow_motion", NULL, CMD_OPT_SLOW_MOTION, true, CMD_EVENT_NONE,
"Slow motion is running." },
{ "mute", NULL, CMD_OPT_MUTE, true, CMD_EVENT_AUDIO_MUTE_TOGGLE,
"Audio is muted." },
{ "fps", NULL, CMD_OPT_FPS, true, CMD_EVENT_FPS_TOGGLE,
"The framerate is shown on screen." },
{ "statistics", NULL, CMD_OPT_STATISTICS, true,
CMD_EVENT_STATISTICS_TOGGLE,
"The technical statistics are shown on screen." },
{ "fullscreen", "video_fullscreen", CMD_OPT_FULLSCREEN, true,
CMD_EVENT_FULLSCREEN_TOGGLE, "The window is fullscreen." },
#if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL)
{ "shader", NULL, CMD_OPT_SHADER, true, CMD_EVENT_SHADER_TOGGLE,
"The shader preset is applied." },
#endif
#ifdef HAVE_VIDEO_FILTER
{ "video_filter", NULL, CMD_OPT_VIDEO_FILTER, true,
CMD_VIDEO_FILTER_TOGGLE, "The video filter is applied." },
#endif
#ifdef HAVE_REWIND
{ "rewind", NULL, CMD_OPT_REWIND, true, CMD_EVENT_REWIND_TOGGLE,
"Rewind is available." },
#endif
#ifdef HAVE_RUNAHEAD
{ "runahead", NULL, CMD_OPT_RUNAHEAD, true, CMD_EVENT_RUNAHEAD_TOGGLE,
"Run-ahead is on." },
{ "preempt", NULL, CMD_OPT_PREEMPT, true, CMD_EVENT_PREEMPT_TOGGLE,
"Preemptive frames are on." },
#endif
{ "vrr_runloop", NULL, CMD_OPT_VRR_RUNLOOP, true,
CMD_EVENT_VRR_RUNLOOP_TOGGLE,
"The runloop syncs to the exact content framerate." },
{ "turbo_fire", NULL, CMD_OPT_TURBO_FIRE, true,
CMD_EVENT_TURBO_FIRE_TOGGLE, "Turbo fire is on." },
{ "grab_mouse", NULL, CMD_OPT_GRAB_MOUSE, true,
CMD_EVENT_GRAB_MOUSE_TOGGLE, "The mouse is grabbed." },
{ "game_focus", NULL, CMD_OPT_GAME_FOCUS, true, CMD_EVENT_NONE,
"The game has all keyboard input, rather than the hotkeys." },
{ "recording", NULL, CMD_OPT_RECORDING, true, CMD_EVENT_NONE,
"Video is being recorded to a file." },
{ "streaming", NULL, CMD_OPT_STREAMING, true, CMD_EVENT_NONE,
"The session is being streamed." },
#ifdef HAVE_CHEEVOS
{ "cheevos_hardcore", NULL, CMD_OPT_CHEEVOS_HARDCORE, true,
CMD_EVENT_CHEEVOS_HARDCORE_MODE_TOGGLE,
"Achievements hardcore mode is on: it forbids states, rewind and "
"frame advance." },
{ "cheevos_enable", "cheevos_enable", CMD_OPT_CHEEVOS_ENABLE, false,
CMD_EVENT_NONE, "Achievements are enabled." },
#endif
/* Reported, never set. */
{ "loading", NULL, CMD_OPT_LOADING, false, CMD_EVENT_NONE,
"Content is loading: a command that needs the core waits for it." },
{ "frame_count", NULL, CMD_OPT_FRAME_COUNT, false, CMD_EVENT_NONE,
"Frames shown since the video driver started." },
{ "state_slot", NULL, CMD_OPT_STATE_SLOT, false, CMD_EVENT_NONE,
"The save state slot LOAD_STATE and SAVE_STATE use." },
{ "replay_slot", NULL, CMD_OPT_REPLAY_SLOT, false, CMD_EVENT_NONE,
"The replay slot PLAY_REPLAY and RECORD_REPLAY use." },
{ "active_replay", "active_replay", CMD_OPT_ACTIVE_REPLAY, false,
CMD_EVENT_NONE,
"The open replay: its identifier, state flags and frame, or zeroes." },
{ "system", NULL, CMD_OPT_SYSTEM, false, CMD_EVENT_NONE,
"The running core's system, or empty when none is loaded." },
{ "content", NULL, CMD_OPT_CONTENT, false, CMD_EVENT_NONE,
"The running content's path, or empty when none is loaded." },
{ "savefile_directory", "savefile_directory", CMD_OPT_DIR_SAVEFILE,
false, CMD_EVENT_NONE, "Where save files are written." },
{ "savestate_directory", "savestate_directory", CMD_OPT_DIR_SAVESTATE,
false, CMD_EVENT_NONE, "Where save states are written." },
{ "runtime_log_directory", "runtime_log_directory",
CMD_OPT_DIR_RUNTIME_LOG, false, CMD_EVENT_NONE,
"Where runtime logs are written." },
{ "log_dir", "log_dir", CMD_OPT_DIR_LOG, false, CMD_EVENT_NONE,
"Where the log is written." },
{ "cache_directory", "cache_directory", CMD_OPT_DIR_CACHE, false,
CMD_EVENT_NONE, "Where extracted content is cached." },
{ "system_directory", "system_directory", CMD_OPT_DIR_SYSTEM, false,
CMD_EVENT_NONE, "Where cores look for system files." },
{ "netplay_nickname", "netplay_nickname", CMD_OPT_NICKNAME, false,
CMD_EVENT_NONE, "The name shown to netplay peers." }
};
/* The option @name, by its own name or the one GET_CONFIG_PARAM knows
* it by when @alias, or NULL. */
static const struct cmd_option_map *command_option_find(const char *name,
bool alias)
{
size_t i;
for (i = 0; i < ARRAY_SIZE(option_map); i++)
{
if (string_is_equal(name, option_map[i].str))
return &option_map[i];
if ( alias && option_map[i].alias
&& string_is_equal(name, option_map[i].alias))
return &option_map[i];
}
return NULL;
}
/* GET_OPTION [option]
*
* Every option and its value, name and value tab separated, one per
* line; with a name, that option's line alone. */
bool command_get_option(command_t *cmd, const char *arg)
{
char value[PATH_MAX_LENGTH];
size_t i;
size_t _len = 0;
bool found = false;
bool one;
size_t cap;
char *reply;
while (arg && *arg == ' ')
arg++;
one = arg && *arg;
/* one line needs room for one; the whole table stops short of what a
* reply can carry, as the other lists do */
cap = one ? NAME_MAX_LENGTH + PATH_MAX_LENGTH + 2
: COMMAND_LIST_REPLY_MAX;
if (!(reply = (char*)malloc(cap + 1)))
{
command_reply_error(cmd, "GET_OPTION", "out of memory");
return false;
}
reply[0] = '\0';
for (i = 0; i < ARRAY_SIZE(option_map); i++)
{
int n;
if (one && !string_is_equal(arg, option_map[i].str))
continue;
found = true;
command_option_read(&option_map[i], value, sizeof(value), "on", "off");
n = snprintf(reply + _len, cap - _len, "%s\t%s\n",
option_map[i].str, value);
/* Short of a reply holding every option, half the list would be
* read as the whole of it, so say so instead. */
if (n < 0 || (size_t)n >= cap - _len)
{
free(reply);
command_reply_error(cmd, "GET_OPTION",
"the options did not fit one reply, stopping at %s",
option_map[i].str);
return false;
}
_len += (size_t)n;
}
if (!found)
{
free(reply);
command_reply_error(cmd, "GET_OPTION",
"no such option %s; OPTION show lists them", arg);
return false;
}
cmd->replier(cmd, reply, _len);
free(reply);
return true;
}
/* SET_OPTION