Files
RetroArch/command.c
T
LibretroAdmin 1b86833a76 command: SET_INPUT and RELEASE_INPUT ask the input driver by name
They took the input state to write what a command holds, two more
takes than command.c is allowed. input_driver_command_hold(),
_release() and _held() own it now. input_state_grab_check passes
again.
2026-10-13 00:38:46 +02:00

4937 lines
155 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 <stdarg.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 "playlist.h"
#include <queues/task_queue.h>
#include "cheat_manager.h"
#include "content.h"
#include "dynamic.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 <compat/strl.h>
#ifdef __MACH__
#include <TargetConditionals.h>
#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);
}
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;
/* 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;
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);
}
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, 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 <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];
/* 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 ... */
/* 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;
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();
}
/* 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)
{
if (msg && input_driver_command_generation() == d->gen)
{
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(d->dest);
d->dest = NULL;
}
#ifdef HAVE_SCREENSHOTS
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;
char msg[PATH_MAX_LENGTH];
if (error || !r)
{
snprintf(msg, sizeof(msg), "SCREENSHOT ERROR %s",
error ? error : "no result");
command_deferred_send(d, msg, strlen(msg), true);
}
else if (r->png_base64 && d->cmd->reply_image_to && d->dest
&& input_driver_command_generation() == d->gen)
{
/* the picture itself, with its path, where the interface takes one */
d->cmd->reply_image_to(d->cmd, d->dest, r->path, strlen(r->path),
"image/png", r->png_base64, r->png_base64_len);
command_deferred_send(d, NULL, 0, false);
}
else
{
strlcpy(msg, r->path, sizeof(msg));
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;
}
#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;
/* what succeeded loaded a state */
bool state_loaded;
};
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();
}
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;
if (!(r->start ? r->start(r) : command_event(r->event, &notify)))
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)
{
struct command_reply *r;
if ( !cmd->structured
|| (!next && !task_content_load_pending())
|| !(r = command_reply_new(name)))
return true;
r->want_core = want_core;
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;
}
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 = 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;
}
/* 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;
if (!arg || !*arg)
return false;
v = strtoul(arg, &end, 10);
if (end == arg)
return false;
while (*end == ' ')
end++;
if (*end)
return false;
*out = (unsigned)v;
return true;
}
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))
{
char msg[PATH_MAX_LENGTH + 64];
snprintf(msg, sizeof(msg), "%s \"%s\".",
msg_hash_to_str(MSG_FAILED_TO_SAVE_STATE_TO), r->path);
command_reply_done(NULL, NULL, r, 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_uint(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;
}
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
char replay_path[16384];
unsigned int slot = 0;
struct command_reply *r = NULL;
if (!command_arg_uint(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(replay_path, sizeof(replay_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(), replay_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
#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 && nbytes <= COMMAND_READ_NBYTES_MAX))
{
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)
{
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
{
/* 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 = (unsigned int)strtoul(arg, (char**)&arg, 16);
unsigned int avail = 0;
uint8_t *data = (uint8_t *)rcheevos_patch_address_avail(addr, &avail);
const uint8_t *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 (*arg && avail)
{
*data = strtoul(arg, (char**)&arg, 16);
data++;
avail--;
}
if (*arg)
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_MENU,
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_MENU:
#ifdef HAVE_MENU
{
struct menu_state *menu_st = menu_state_get_ptr();
on = menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE);
}
#endif
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." },
#ifdef HAVE_MENU
{ "menu", "menu_active", CMD_OPT_MENU, true, CMD_EVENT_MENU_TOGGLE,
"The menu is open." },
#endif
#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_STREAMING_TOGGLE, "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; GET_OPTION with no argument lists them", arg);
return false;
}
cmd->replier(cmd, reply, _len);
free(reply);
return true;
}
/* SET_OPTION <option> on|off
*
* Asking for the state the option is already in changes nothing and
* succeeds, so the same request twice is one change. */
bool command_set_option(command_t *cmd, const char *arg)
{
char reply[NAME_MAX_LENGTH + 32];
size_t _len;
const struct cmd_option_map *o;
const char *sp;
char name[NAME_MAX_LENGTH];
bool on;
while (arg && *arg == ' ')
arg++;
if (!arg || !*arg || !(sp = strchr(arg, ' ')))
{
command_reply_error(cmd, "SET_OPTION",
"an option and on or off, as in SET_OPTION pause on");
return false;
}
if ((size_t)(sp - arg) >= sizeof(name))
{
command_reply_error(cmd, "SET_OPTION", "no such option");
return false;
}
memcpy(name, arg, (size_t)(sp - arg));
name[sp - arg] = '\0';
while (*sp == ' ')
sp++;
if (string_is_equal(sp, "on"))
on = true;
else if (string_is_equal(sp, "off"))
on = false;
else
{
command_reply_error(cmd, "SET_OPTION",
"%s takes on or off, not %s", name, sp);
return false;
}
if (!(o = command_option_find(name, false)))
{
command_reply_error(cmd, "SET_OPTION",
"no such option %s; GET_OPTION with no argument lists them",
name);
return false;
}
if (!o->settable)
{
command_reply_error(cmd, "SET_OPTION", "%s only reports", o->str);
return false;
}
if ( command_option_read(o, NULL, 0, NULL, NULL) != on
&& !command_option_set(o, cmd, on))
{
if (!cmd->error)
command_reply_error(cmd, "SET_OPTION", "%s could not be turned %s",
o->str, on ? "on" : "off");
return false;
}
/* the shape GET_OPTION gives, so one reading serves both */
_len = (size_t)snprintf(reply, sizeof(reply), "%s\t%s\n", o->str,
on ? "on" : "off");
cmd->replier(cmd, reply, _len);
return true;
}
#ifdef HAVE_MCP
/* GET_LOG [since]
*
* The lines logged since line @since (0 when left out), as many as one
* reply holds, then NEXT and the number to ask from next time. Lines
* logged before the first GET_LOG, or the MCP server started, are not
* kept; SKIPPED and a count says lines were overwritten before they
* were read. */
bool command_get_log(command_t *cmd, const char *arg)
{
size_t first, next, _len;
unsigned since = 0;
char *reply;
while (arg && *arg == ' ')
arg++;
if (arg && *arg && !command_arg_uint(arg, &since))
{
command_reply_error(cmd, "GET_LOG", "a line number, not %s", arg);
return false;
}
verbosity_ring_arm();
if (!(reply = (char*)malloc(COMMAND_LIST_REPLY_MAX + 1)))
{
command_reply_error(cmd, "GET_LOG", "out of memory");
return false;
}
/* room kept for the SKIPPED and NEXT lines */
_len = verbosity_ring_read(since, reply + 32, COMMAND_LIST_REPLY_MAX - 64,
&first, &next);
if (first > since)
{
int n = snprintf(reply, 32, "SKIPPED %u\n", (unsigned)(first - since));
memmove(reply + n, reply + 32, _len);
_len += (size_t)n;
}
else
memmove(reply, reply + 32, _len);
_len += (size_t)snprintf(reply + _len, 32, "NEXT %u\n", (unsigned)next);
cmd->replier(cmd, reply, _len);
free(reply);
return true;
}
#endif
/* RUN_FRAMES in progress: frames still to run, and the reply owed */
static struct
{
command_deferred_t deferred;
unsigned asked;
unsigned left;
} command_run_frames_st;
static void command_run_frames_answer(const char *msg, bool is_error)
{
runloop_frame_work_set(RUNLOOP_WORK_RUN_FRAMES, false);
command_run_frames_st.left = 0;
command_deferred_send(&command_run_frames_st.deferred, msg, strlen(msg),
is_error);
}
void command_run_frames_cancel(const char *why)
{
char msg[128];
snprintf(msg, sizeof(msg), "RUN_FRAMES ERROR %s", why);
command_run_frames_answer(msg, true);
}
/* A frame has run: the reply goes once the last of them has */
void command_run_frames_tick(void)
{
char msg[64];
if (--command_run_frames_st.left)
return;
snprintf(msg, sizeof(msg), "RUN_FRAMES %u\n", command_run_frames_st.asked);
command_run_frames_answer(msg, false);
}
/* RUN_FRAMES <count>
*
* Pauses, if the content is running, and runs it @count frames,
* answered once the last has run; it stays paused. Another RUN_FRAMES
* replaces one still running, which is answered with an error. */
bool command_run_frames(command_t *cmd, const char *arg)
{
unsigned n = 0;
runloop_state_t *runloop_st = runloop_state_get_ptr();
#ifdef HAVE_MENU
struct menu_state *menu_st = menu_state_get_ptr();
#endif
if (!command_arg_uint(arg, &n) || n < 1 || n > COMMAND_RUN_FRAMES_MAX)
{
command_reply_error(cmd, "RUN_FRAMES", "a frame count from 1 to %u",
COMMAND_RUN_FRAMES_MAX);
return false;
}
if (!(runloop_st->flags & RUNLOOP_FLAG_CORE_RUNNING))
{
command_reply_error(cmd, "RUN_FRAMES", "no content is running");
return false;
}
#ifdef HAVE_MENU
if (menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE))
{
command_reply_error(cmd, "RUN_FRAMES", "the menu is open");
return false;
}
#endif
#ifdef HAVE_CHEEVOS
if (rcheevos_hardcore_active())
{
command_reply_error(cmd, "RUN_FRAMES",
"achievements hardcore mode forbids frame advance");
return false;
}
#endif
if (runloop_st->frame_work & RUNLOOP_WORK_RUN_FRAMES)
command_run_frames_cancel("replaced by another RUN_FRAMES");
command_run_frames_st.asked = n;
command_run_frames_st.left = n;
command_deferred_take(&command_run_frames_st.deferred, cmd);
runloop_frame_work_set(RUNLOOP_WORK_RUN_FRAMES, true);
return true;
}
/* RetroPad controls by the names SET_INPUT takes them by: the sixteen
* buttons in RETRO_DEVICE_ID_JOYPAD order, then the four stick axes. */
static const char *const command_input_names[] = {
"b", "y", "select", "start", "up", "down", "left", "right",
"a", "x", "l", "r", "l2", "r2", "l3", "r3",
"left_x", "left_y", "right_x", "right_y"
};
/* @port's held controls, the shape SET_INPUT and RELEASE_INPUT answer
* with: the player, then the buttons and the axes held, tab separated,
* "none" for either when there are none. */
static size_t command_input_line(unsigned port, char *s, size_t len)
{
unsigned i;
int16_t analog[4];
uint16_t buttons = input_driver_command_held(port, analog);
size_t _len = (size_t)snprintf(s, len, "%u\t", port + 1);
bool any = false;
for (i = 0; i < 16 && _len < len; i++)
if (buttons & (1u << i))
{
_len += (size_t)snprintf(s + _len, len - _len, "%s%s",
any ? " " : "", command_input_names[i]);
any = true;
}
if (!any && _len < len)
_len += strlcpy(s + _len, "none", len - _len);
if (_len < len)
s[_len++] = '\t';
any = false;
for (i = 0; i < 4 && _len < len; i++)
if (analog[i])
{
_len += (size_t)snprintf(s + _len, len - _len, "%s%s=%d",
any ? " " : "", command_input_names[16 + i],
(int)analog[i]);
any = true;
}
if (!any && _len < len)
_len += strlcpy(s + _len, "none", len - _len);
if (_len + 1 >= len)
_len = len - 2;
s[_len++] = '\n';
s[_len] = '\0';
return _len;
}
/* SET_INPUT <player> <control> <value>
*
* Holds a RetroPad button (1, or 0 to let it go) or a stick axis
* (-32768 to 32767, 0 to let it go) for the core, over the player's own
* input, until it is set again or RELEASE_INPUT lets it go. */
bool command_set_input(command_t *cmd, const char *arg)
{
char reply[192];
char control[16];
unsigned player;
unsigned i;
long value;
char *end = NULL;
const char *sp;
while (arg && *arg == ' ')
arg++;
player = arg ? (unsigned)strtoul(arg, &end, 10) : 0;
if (!end || end == arg || *end != ' ' || player < 1 || player > MAX_USERS)
{
command_reply_error(cmd, "SET_INPUT",
"a player from 1 to %u, a control and a value",
(unsigned)MAX_USERS);
return false;
}
while (*end == ' ')
end++;
if (!(sp = strchr(end, ' ')) || (size_t)(sp - end) >= sizeof(control))
{
command_reply_error(cmd, "SET_INPUT", "a control and a value");
return false;
}
memcpy(control, end, (size_t)(sp - end));
control[sp - end] = '\0';
for (i = 0; i < ARRAY_SIZE(command_input_names); i++)
if (string_is_equal(control, command_input_names[i]))
break;
if (i == ARRAY_SIZE(command_input_names))
{
command_reply_error(cmd, "SET_INPUT", "no control named %s", control);
return false;
}
value = strtol(sp + 1, &end, 10);
if ( end == sp + 1 || *end
|| (i < 16 && value != 0 && value != 1)
|| (i >= 16 && (value < -32768 || value > 32767)))
{
command_reply_error(cmd, "SET_INPUT", i < 16
? "%s takes 1 to hold it or 0 to let it go"
: "%s takes -32768 to 32767", control);
return false;
}
#ifdef HAVE_CHEEVOS
if (rcheevos_hardcore_active())
{
command_reply_error(cmd, "SET_INPUT",
"achievements hardcore mode forbids input from a command");
return false;
}
#endif
input_driver_command_hold(player - 1, i, (int16_t)value);
cmd->replier(cmd, reply,
command_input_line(player - 1, reply, sizeof(reply)));
return true;
}
/* RELEASE_INPUT [player]
*
* Lets go of everything SET_INPUT holds for the player, or for every
* player. */
bool command_release_input(command_t *cmd, const char *arg)
{
char reply[192];
unsigned player = 0;
while (arg && *arg == ' ')
arg++;
if (arg && *arg)
{
if (!command_arg_uint(arg, &player) || player < 1 || player > MAX_USERS)
{
command_reply_error(cmd, "RELEASE_INPUT",
"a player from 1 to %u, or nothing for every player",
(unsigned)MAX_USERS);
return false;
}
}
input_driver_command_release(player ? player - 1 : MAX_USERS);
cmd->replier(cmd, reply,
command_input_line(player ? player - 1 : 0, reply, sizeof(reply)));
return true;
}
/* The core option @key, or false with the reason replied. */
static bool command_core_option_find(command_t *cmd, const char *name,
const char *key, core_option_manager_t **opt, size_t *idx)
{
runloop_state_t *runloop_st = runloop_state_get_ptr();
if (!(*opt = runloop_st ? runloop_st->core_options : NULL) || !(*opt)->size)
{
command_reply_error(cmd, name, "the running core has no options");
return false;
}
if (key && !core_option_manager_get_idx(*opt, key, idx))
{
command_reply_error(cmd, name,
"no such core option %s; GET_CORE_OPTION with no argument lists "
"them", key);
return false;
}
return true;
}
/* @opt's line: key, value and description, tab separated, and its
* values after a tab when @values. Returns what it wrote, or 0 when
* that did not fit @len. */
static size_t command_core_option_line(const struct core_option *opt,
char *s, size_t len, bool values)
{
size_t i;
size_t _len;
int n = snprintf(s, len, "%s\t%s\t%s", opt->key,
opt->vals && opt->index < opt->vals->size
? opt->vals->elems[opt->index].data : "",
opt->desc ? opt->desc : "");
if (n < 0 || (size_t)n >= len)
return 0;
_len = (size_t)n;
if (values && opt->vals)
for (i = 0; i < opt->vals->size; i++)
{
n = snprintf(s + _len, len - _len, "%s%s", i ? "|" : "\t",
opt->vals->elems[i].data);
if (n < 0 || (size_t)n >= len - _len)
return 0;
_len += (size_t)n;
}
if (_len + 1 >= len)
return 0;
s[_len++] = '\n';
s[_len] = '\0';
return _len;
}
/* GET_CORE_OPTION [key]
*
* Every option of the running core, one per line: key, value and
* description, tab separated. With a key, that option's line alone,
* with the values it takes after a further tab, separated by |. */
bool command_get_core_option(command_t *cmd, const char *arg)
{
core_option_manager_t *opt = NULL;
size_t idx = 0;
size_t _len = 0;
size_t i;
char *reply;
while (arg && *arg == ' ')
arg++;
if (!command_core_option_find(cmd, "GET_CORE_OPTION",
arg && *arg ? arg : NULL, &opt, &idx))
return false;
if (!(reply = (char*)malloc(COMMAND_LIST_REPLY_MAX + 1)))
{
command_reply_error(cmd, "GET_CORE_OPTION", "out of memory");
return false;
}
for (i = (arg && *arg) ? idx : 0; i < opt->size; i++)
{
size_t n = command_core_option_line(&opt->opts[i], reply + _len,
COMMAND_LIST_REPLY_MAX + 1 - _len, arg && *arg);
if (!n)
{
free(reply);
command_reply_error(cmd, "GET_CORE_OPTION",
"the options did not fit one reply, stopping at %s",
opt->opts[i].key);
return false;
}
_len += n;
if (arg && *arg)
break;
}
cmd->replier(cmd, reply, _len);
free(reply);
return true;
}
/* SET_CORE_OPTION <key> <value>
*
* The value is one GET_CORE_OPTION <key> lists. Asking for the value
* the option already has changes nothing and succeeds. */
bool command_set_core_option(command_t *cmd, const char *arg)
{
char key[NAME_MAX_LENGTH];
char reply[NAME_MAX_LENGTH + PATH_MAX_LENGTH];
core_option_manager_t *opt = NULL;
const char *sp;
size_t idx = 0;
size_t val_idx = 0;
size_t _len;
while (arg && *arg == ' ')
arg++;
if (!arg || !*arg || !(sp = strchr(arg, ' ')))
{
command_reply_error(cmd, "SET_CORE_OPTION",
"a core option key and a value");
return false;
}
if ((size_t)(sp - arg) >= sizeof(key))
{
command_reply_error(cmd, "SET_CORE_OPTION", "no such core option");
return false;
}
memcpy(key, arg, (size_t)(sp - arg));
key[sp - arg] = '\0';
while (*sp == ' ')
sp++;
if (!command_core_option_find(cmd, "SET_CORE_OPTION", key, &opt, &idx))
return false;
if (!core_option_manager_get_val_idx(opt, idx, sp, &val_idx))
{
command_reply_error(cmd, "SET_CORE_OPTION",
"%s does not take %s; GET_CORE_OPTION %s lists what it takes",
key, sp, key);
return false;
}
if (opt->opts[idx].index != val_idx)
core_option_manager_set_val(opt, idx, val_idx, true);
/* the shape GET_CORE_OPTION gives, so one reading serves both */
if (!(_len = command_core_option_line(&opt->opts[idx], reply,
sizeof(reply), false)))
_len = (size_t)snprintf(reply, sizeof(reply), "%s\t%s\n", key, sp);
cmd->replier(cmd, reply, _len);
return true;
}
/* GET_CONFIG_PARAM <param name>
*
* The option that goes by this name, which GET_OPTION also reports;
* "unsupported" for one this build does not have, as it always was.
* Booleans are true and false here, the words this interface has always
* answered, where GET_OPTION says on and off. */
bool command_get_config_param(command_t *cmd, const char* arg)
{
char value[PATH_MAX_LENGTH];
char reply[PATH_MAX_LENGTH + NAME_MAX_LENGTH];
size_t _len;
const struct cmd_option_map *o;
while (arg && *arg == ' ')
arg++;
if (!arg)
arg = "";
if ((o = command_option_find(arg, true)))
command_option_read(o, value, sizeof(value), "true", "false");
else
strlcpy(value, "unsupported", sizeof(value));
_len = (size_t)snprintf(reply, sizeof(reply), "GET_CONFIG_PARAM %s %s",
arg, value);
cmd->replier(cmd, reply, _len);
return true;
}
/* LIST_CORES
*
* The installed cores, one per line: display name, a tab, the path
* LOAD_CONTENT takes. The list is already in memory. */
bool command_list_cores(command_t *cmd, const char *arg)
{
core_info_list_t *list = NULL;
size_t i, len = 0;
char *reply;
if (!(reply = (char*)malloc(COMMAND_LIST_REPLY_MAX + 64)))
return false;
reply[0] = '\0';
if (core_info_get_list(&list) && list)
for (i = 0; i < list->count; i++)
{
const core_info_t *ci = &list->list[i];
int n;
if (!ci->path)
continue;
n = snprintf(reply + len, COMMAND_LIST_REPLY_MAX - len, "%s\t%s\n",
ci->display_name ? ci->display_name : "", ci->path);
if (n < 0 || (size_t)n >= COMMAND_LIST_REPLY_MAX - len)
{
len += (size_t)snprintf(reply + len, 64, "MORE\n");
break;
}
len += (size_t)n;
}
if (!len)
len = strlcpy(reply, "LIST_CORES none\n", COMMAND_LIST_REPLY_MAX);
cmd->replier(cmd, reply, len);
free(reply);
return true;
}
/* LIST_PLAYLISTS and GET_PLAYLIST read files, which may be slow, so
* they answer from a task: the text is made on the task thread and sent
* from the callback. */
struct command_query
{
command_deferred_t deferred;
size_t first; /* GET_PLAYLIST: first entry */
char *reply; /* made by the handler */
size_t reply_len;
char dir[PATH_MAX_LENGTH];
char name[NAME_MAX_LENGTH];
bool list_playlists;
/* the reply reports a failure: no such playlist */
bool error;
/* the playlist format settings, taken on the main thread */
bool old_format;
bool compress;
bool fuzzy_archive_match;
bool portable_paths;
char content_dir[DIR_MAX_LENGTH];
};
static void command_query_append(struct command_query *q, size_t *cap,
const char *s)
{
size_t n = strlen(s);
if (!q->reply || q->reply_len + n + 1 > *cap)
return;
memcpy(q->reply + q->reply_len, s, n + 1);
q->reply_len += n;
}
static void command_query_handler(retro_task_t *task)
{
struct command_query *q = (struct command_query*)task->state;
size_t cap = COMMAND_LIST_REPLY_MAX;
if ((q->reply = (char*)malloc(cap + 1)))
q->reply[0] = '\0';
if (q->list_playlists)
{
struct string_list *files = dir_list_new(q->dir, "lpl",
false, false, false, false);
size_t i;
for (i = 0; files && i < files->size; i++)
{
char line[NAME_MAX_LENGTH + 2];
const char *base = path_basename(files->elems[i].data);
snprintf(line, sizeof(line), "%s\n", base);
if (q->reply_len + strlen(line) + 6 > cap)
{
command_query_append(q, &cap, "MORE\n");
break;
}
command_query_append(q, &cap, line);
}
if (files)
string_list_free(files);
if (!q->reply_len)
command_query_append(q, &cap, "LIST_PLAYLISTS none\n");
}
else
{
char path[PATH_MAX_LENGTH];
playlist_config_t config;
playlist_t *pl;
fill_pathname_join_special(path, q->dir, q->name, sizeof(path));
if (!string_ends_with_size(path, ".lpl", strlen(path), 4))
strlcat(path, ".lpl", sizeof(path));
config.capacity = COLLECTION_SIZE;
config.old_format = q->old_format;
config.compress = q->compress;
config.fuzzy_archive_match = q->fuzzy_archive_match;
playlist_config_set_path(&config, path);
playlist_config_set_base_content_directory(&config,
q->portable_paths ? q->content_dir : NULL);
if (!path_is_valid(path) || !(pl = playlist_init(&config)))
{
q->error = true;
command_query_append(q, &cap, "GET_PLAYLIST ERROR no such playlist\n");
}
else
{
size_t i, n = playlist_size(pl);
for (i = q->first; i < n; i++)
{
const struct playlist_entry *e = NULL;
char line[3 * PATH_MAX_LENGTH];
if (i - q->first >= COMMAND_PLAYLIST_PAGE)
{
snprintf(line, sizeof(line), "MORE %u\n", (unsigned)i);
command_query_append(q, &cap, line);
break;
}
playlist_get_index(pl, i, &e);
if (!e)
continue;
snprintf(line, sizeof(line), "%u\t%s\t%s\t%s\n", (unsigned)i,
e->label ? e->label : "",
e->path ? e->path : "",
e->core_path ? e->core_path : "");
if (q->reply_len + strlen(line) + 16 > cap)
{
snprintf(line, sizeof(line), "MORE %u\n", (unsigned)i);
command_query_append(q, &cap, line);
break;
}
command_query_append(q, &cap, line);
}
if (!q->reply_len)
command_query_append(q, &cap, "GET_PLAYLIST empty\n");
playlist_free(pl);
}
}
task_set_progress(task, 100);
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
}
static void command_query_callback(retro_task_t *task, void *task_data,
void *user_data, const char *error)
{
struct command_query *q = (struct command_query*)task->state;
if (q)
command_deferred_send(&q->deferred, q->reply, q->reply_len, q->error);
}
static void command_query_cleanup(retro_task_t *task)
{
struct command_query *q = (struct command_query*)task->state;
if (!q)
return;
free(q->reply);
free(q->deferred.dest);
free(q);
task->state = NULL;
}
static bool command_query_push(command_t *cmd, bool list_playlists,
const char *arg)
{
settings_t *settings = config_get_ptr();
struct command_query *q;
retro_task_t *task;
if (!(q = (struct command_query*)calloc(1, sizeof(*q))))
return false;
q->list_playlists = list_playlists;
strlcpy(q->dir, settings->paths.directory_playlist, sizeof(q->dir));
q->old_format = settings->bools.playlist_use_old_format;
q->compress = settings->bools.playlist_compression;
q->fuzzy_archive_match = settings->bools.playlist_fuzzy_archive_match;
q->portable_paths = settings->bools.playlist_portable_paths;
strlcpy(q->content_dir, settings->paths.directory_menu_content,
sizeof(q->content_dir));
if (!list_playlists)
{
/* "<name> [first]": the name may hold spaces, so a last word that
* is a number and nothing else is the first entry wanted. It has
* to be the whole word: a playlist whose name ends in one, as
* "Atari - 2600" and "Sony - PlayStation 2" do, was read as a
* name without it and an entry to start at. */
const char *sp;
while (*arg == ' ')
arg++;
strlcpy(q->name, arg, sizeof(q->name));
if ((sp = strrchr(q->name, ' ')) && sp[1] >= '0' && sp[1] <= '9')
{
char *end = NULL;
unsigned long first = strtoul(sp + 1, &end, 10);
/* the whole word, or it belongs to the name */
if (end && !*end)
{
q->first = (size_t)first;
q->name[sp - q->name] = '\0';
}
}
if (!*q->name || strchr(q->name, '/') || strchr(q->name, '\\'))
{
command_reply_error(cmd, "GET_PLAYLIST",
"a playlist name, as LIST_PLAYLISTS gives");
free(q);
return false;
}
}
if (!(task = task_init()))
{
free(q);
return false;
}
command_deferred_take(&q->deferred, cmd);
task->state = q;
task->handler = command_query_handler;
task->callback = command_query_callback;
task->cleanup = command_query_cleanup;
task->flags |= RETRO_TASK_FLG_MUTE;
task_queue_push(task);
return true;
}
/* LIST_PLAYLISTS: the playlist files, one per line, as GET_PLAYLIST
* takes them. */
bool command_list_playlists(command_t *cmd, const char *arg)
{
return command_query_push(cmd, true, arg);
}
/* GET_PLAYLIST <playlist> [first]: a playlist's entries, one per line:
* index, label, content path, core path, tab separated;
* COMMAND_PLAYLIST_PAGE at a time, "MORE <next>" when there are more. */
bool command_get_playlist(command_t *cmd, const char *arg)
{
return command_query_push(cmd, false, arg);
}
/* One HELP line: name, argument, description, and what it does to
* RetroArch, appended to @s at *@len; false when it would not fit. */
static bool command_help_line(char *s, size_t size, size_t *len,
const char *name, const char *arg_desc, const char *desc,
unsigned flags)
{
int n = snprintf(s + *len, size - *len, "%s%s%s\t%s%s\n",
name,
arg_desc ? " " : "", arg_desc ? arg_desc : "",
desc ? desc : "",
(flags & CMD_INFO_READ_ONLY) ? " [read-only]"
: (flags & CMD_INFO_DESTRUCTIVE) ? " [destructive]"
: "");
if (n < 0 || (size_t)n >= size - *len)
return false;
*len += (size_t)n;
return true;
}
/* HELP [command]
*
* With no argument, every command, one per line: the name, its
* argument, a tab, what it does, and [read-only] or [destructive].
* Hotkey commands take no argument and press the hotkey for one
* frame. With a command name, that command's line alone. */
bool command_help(command_t *cmd, const char* arg)
{
size_t i;
size_t _len = 0;
size_t size = 256
+ (ARRAY_SIZE(map) + ARRAY_SIZE(action_map)) * 160;
bool found = false;
char *reply;
while (arg && *arg == ' ')
arg++;
if (!(reply = (char*)malloc(size)))
return false;
reply[0] = '\0';
for (i = 0; i < ARRAY_SIZE(action_map); i++)
{
bool noarg = string_is_equal(action_map[i].arg_desc, "No argument");
if (arg && *arg && !string_is_equal(arg, action_map[i].str))
continue;
found = true;
if (!command_help_line(reply, size, &_len, action_map[i].str,
noarg ? NULL : action_map[i].arg_desc,
action_map[i].desc, action_map[i].flags))
break;
}
for (i = 0; i < ARRAY_SIZE(map); i++)
{
if (arg && *arg && !string_is_equal(arg, map[i].str))
continue;
found = true;
if (!command_help_line(reply, size, &_len, map[i].str, NULL,
map[i].desc, map[i].flags))
break;
}
if (!found)
_len = (size_t)snprintf(reply, size, "HELP ERROR unknown command %s\n",
arg);
cmd->replier(cmd, reply, _len);
free(reply);
return found;
}
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);
if (!task_push_start_current_core(&content_info))
return false;
return command_content_reply(cmd, "START_CORE", false, true);
}
/* 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));
if (!task_push_load_content_with_new_core_from_companion_ui(
core_path, exp, NULL, NULL, NULL, &content_info, NULL, NULL))
return false;
}
#else
if (!task_push_load_content_with_new_core_from_companion_ui(
core_path, sep + 1, NULL, NULL, NULL, &content_info, NULL, NULL))
return false;
#endif
return command_content_reply(cmd, "LOAD_CONTENT", false, true);
}
/* 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
/* this quits instead, and there is no unload to wait for */
if (should_quit_on_close())
return command_event(CMD_EVENT_CLOSE_CONTENT, NULL);
/* the menu unloads the core on a later frame */
if (!command_event(CMD_EVENT_CLOSE_CONTENT, NULL))
return false;
return command_content_reply(cmd, "CLOSE_CONTENT", true, false);
#else
if (!command_event(CMD_EVENT_UNLOAD_CORE, NULL))
return false;
return command_content_reply(cmd, "CLOSE_CONTENT", false, false);
#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;
command_content_reply(cmd, "UNLOAD_CORE", false, 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;
}
/* @v in lowercase hexadecimal, whatever the width of size_t */
static void command_hex(char *s, size_t v)
{
char tmp[24];
size_t n = 0;
do
{
tmp[n++] = "0123456789abcdef"[v & 15];
v >>= 4;
} while (v);
while (n)
*s++ = tmp[--n];
*s = '\0';
}
/* GET_MEMORY_MAP
*
* The running core's memory map, one region per line, tab separated:
* the first system address READ_CORE_MEMORY takes in it, its length,
* its select and disconnect masks (all hexadecimal), what it holds
* (system, save, video or other), rw, ro, or none when nothing backs
* it, its byte order, and its address space name. */
bool command_get_memory_map(command_t *cmd, const char *arg)
{
unsigned i;
size_t _len = 0;
char *reply;
runloop_state_t *runloop_st = runloop_state_get_ptr();
const rarch_memory_map_t *mmap = runloop_st ? &runloop_st->system.mmaps
: NULL;
if (!mmap || !mmap->num_descriptors)
{
command_reply_error(cmd, "GET_MEMORY_MAP",
"the running core has no memory map");
return false;
}
if (!(reply = (char*)malloc(COMMAND_LIST_REPLY_MAX + 1)))
{
command_reply_error(cmd, "GET_MEMORY_MAP", "out of memory");
return false;
}
for (i = 0; i < mmap->num_descriptors; i++)
{
const struct retro_memory_descriptor *d = &mmap->descriptors[i].core;
const char *kind = (d->flags & RETRO_MEMDESC_SYSTEM_RAM) ? "system"
: (d->flags & RETRO_MEMDESC_SAVE_RAM) ? "save"
: (d->flags & RETRO_MEMDESC_VIDEO_RAM) ? "video"
: "other";
const char *access = !d->ptr ? "none"
: (d->flags & RETRO_MEMDESC_CONST) ? "ro" : "rw";
char start[24], len[24], sel[24], disc[24];
int n;
command_hex(start, d->start);
command_hex(len, d->len);
command_hex(sel, d->select);
command_hex(disc, d->disconnect);
n = snprintf(reply + _len, COMMAND_LIST_REPLY_MAX + 1 - _len,
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
start, len, sel, disc, kind, access,
(d->flags & RETRO_MEMDESC_BIGENDIAN) ? "big" : "little",
d->addrspace ? d->addrspace : "");
if (n < 0 || (size_t)n >= COMMAND_LIST_REPLY_MAX + 1 - _len)
{
free(reply);
command_reply_error(cmd, "GET_MEMORY_MAP",
"the map did not fit one reply, stopping at region %u", i);
return false;
}
_len += (size_t)n;
}
cmd->replier(cmd, reply, _len);
free(reply);
return true;
}
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 == ' '))
{
command_reply_error(cmd, "READ_CORE_MEMORY",
"a hexadecimal address, a space and a byte count");
return false;
}
nbytes = (unsigned int)strtoul(end + 1, NULL, 10);
if (nbytes == 0 || nbytes > COMMAND_READ_NBYTES_MAX)
{
command_reply_error(cmd, "READ_CORE_MEMORY",
"a byte count from 1 to %u", COMMAND_READ_NBYTES_MAX);
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)
{
command_reply_error(cmd, "READ_CORE_MEMORY", "out of memory");
return false;
}
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
{
/* command_memory_get_pointer() has put the reason in the reply,
* in the form line clients have always read */
_len = strlen(reply);
cmd->error = true;
}
cmd->replier(cmd, reply, _len);
free(reply);
return !cmd->error;
}
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
}
else
/* command_memory_get_pointer() has put the reason in the reply,
* in the form line clients have always read; nothing was written */
cmd->error = true;
cmd->replier(cmd, reply, strlen(reply));
return !cmd->error;
}
#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
|| (video_driver_get_disp_flags() & VIDEO_FLAG_FORCE_FULLSCREEN);
if (window_scale == 0)
return false;
configuration_set_uint(settings, settings->uints.video_scale, window_scale);
if (!video_fullscreen)
{
if ( !video_driver_test_all_flags(GFX_CTX_FLAGS_WINDOW_RESIZE_IN_PLACE)
|| !video_driver_set_video_mode(
video_driver_window_dims(false), false))
{
int reinit_flags = DRIVER_VIDEO_AND_INPUT_MASK;
command_event(CMD_EVENT_REINIT, &reinit_flags);
}
}
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;
bool must_initialize = (core_serialization_quirks()
& RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE) != 0;
/* 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
if (must_initialize
&& runloop_st->auto_state_load_attempted
&& !runloop_st->auto_state_load_ready)
return false;
_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;
if (must_initialize && !runloop_st->auto_state_load_ready)
{
if (!runloop_st->auto_state_load_pending)
{
runloop_st->auto_state_load_pending = true;
RARCH_LOG("[State] Auto-loading savestate \"%s\" deferred until core initialization completes.\n",
savestate_name_auto);
}
return true;
}
if (must_initialize)
{
runloop_st->auto_state_load_pending = false;
runloop_st->auto_state_load_attempted = true;
}
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;
char abs_arg[PATH_MAX_LENGTH];
#ifdef HAVE_COMMAND
struct command_reply *r;
#endif
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)))
{
command_reply_error(cmd, "SET_SHADER",
"the video driver cannot run this kind of shader");
return false;
}
/* rebase on shader directory */
if (!path_is_absolute(arg))
{
const char *ref_path = settings->paths.directory_video_shader;
fill_pathname_join_special(abs_arg, ref_path, arg, sizeof(abs_arg));
arg = abs_arg;
}
/* drivers fall back to the stock shader for a preset they
* cannot read, and call that success */
if (!path_is_valid(arg))
{
command_reply_error(cmd, "SET_SHADER", "no shader preset at %s",
arg);
return false;
}
}
if (!video_shader_apply_shader(settings, type, arg, true))
{
command_reply_error(cmd, "SET_SHADER",
apply_new_shader ? "the preset could not be applied"
: "the shader could not be turned off");
return false;
}
#ifdef HAVE_COMMAND
/* A deferred load compiles over the next frames: answered once it
* is through. */
if (cmd && cmd->structured && (r = command_reply_new("SET_SHADER")))
{
if (video_shader_deferred_notify(command_reply_done, r))
{
command_deferred_take(&r->deferred, cmd);
return true;
}
free(r);
}
#endif
return 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 dims;
size_t p, size;
};
static void command_reinit_snapshot_cb(void *userdata,
const void *data,
unsigned dims, size_t pitch)
{
struct command_reinit_snapshot_ctx *ctx
= (struct command_reinit_snapshot_ctx*)userdata;
size_t want;
if (!ctx || !data || !VIDEO_SCALE_W(dims) || !VIDEO_SCALE_H(dims) || !pitch)
return;
want = pitch * VIDEO_SCALE_H(dims);
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->dims = dims;
ctx->p = pitch;
ctx->size = want;
}
void command_event_reinit(const int flags)
{
settings_t *settings = config_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;
/* 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_dims = 0;
size_t cached_snapshot_p = 0;
size_t cached_snapshot_size = 0;
/* A reinit while the video driver is down must not create a
* driver instance. It happens when content loads over running
* content: core deinit tears the drivers down, then unloading
* the old content's override fires CMD_EVENT_REINIT (the
* override changed video_fullscreen, and CORE_RUNNING is still
* set). An instance created here is replaced by
* retroarch_main_init's drivers_init without being freed - an
* orphaned window and device that widget fonts keep drawing
* into - while the next drivers_init applies the restored mode
* anyway. Guarded here, in the layer that owns reinit, so every
* caller is covered and call sites stay bare command_events.
*
* The reasons the pointer is captured for are kept above the
* drivers, so there is nothing to put right about the grab here:
* the next video driver has them applied to it. */
if (!video_st->data)
return;
/* A staged content load ends in a reinit of every driver against
* the new core: one asked for before then - the old override
* unloading, the new core's SET_SYSTEM_AV_INFO - would rebuild
* the drivers the stage is about to rebuild again. */
if (runloop_is_content_switching())
return;
{
struct command_reinit_snapshot_ctx ctx;
ctx.buf_p = &cached_snapshot;
ctx.cap_p = &cached_snapshot_cap;
ctx.dims = 0;
ctx.p = 0;
ctx.size = 0;
video_driver_cached_frame_read(&ctx, command_reinit_snapshot_cb);
cached_snapshot_dims = ctx.dims;
cached_snapshot_p = ctx.p;
cached_snapshot_size = ctx.size;
}
(void)cached_snapshot_size;
video_driver_reinit(flags);
/* The hotkey that asked for this may still be down, and is not to
* ask again when the new window gets the focus. */
input_driver_hold_over_window_change();
/* 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 && VIDEO_SCALE_H(cached_snapshot_dims))
{
video_driver_cached_frame_publish(cached_snapshot,
cached_snapshot_dims, 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)
{
unsigned output_size = VIDEO_DRIVER_OUTPUT_DIMS(video_st);
if ( menu_st->driver_ctx
&& menu_st->driver_ctx->render)
menu_st->driver_ctx->render(
menu_st->userdata, output_size, 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. */
input_driver_poll_devices();
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);
}
/* The menu runs with vsync if vsync is on - the rule
* menu_driver_toggle() applies when the menu comes up - and only
* then. This used to switch it on whenever the menu was up, whatever
* the setting: with vsync off, every reinit with the menu up (a
* fullscreen toggle, say) started the new driver without vsync, as
* configured, and a few frames later turned it on here, which
* rebuilds the swapchain that had just been made. The menu ran with
* a vsync the user had switched off, unlike at startup, and on at
* least one Windows Vulkan setup the first present after that second
* swapchain failed or left a dead picture. */
if ( (menu_st->flags & MENU_ST_FLAG_ALIVE)
&& settings->bools.video_vsync
&& !settings->bools.video_scanline_sync
&& 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
}