mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
rcheevos_patch_address returns a pointer with no length, so READ_CORE_RAM served whatever byte count the request asked for and WRITE_CORE_RAM wrote every byte in the payload, both running past the end of the core's memory region. READ_CORE_RAM also computed its reply buffer as `40 + nbytes * 3` in unsigned int: a request for ~1431655766 bytes wrapped that to a few bytes and the reply loop then wrote 4 GiB through it. Add rcheevos_patch_address_avail, a thin wrapper over rc_libretro_memory_find_avail that also reports how many bytes remain in the region. READ_CORE_RAM clamps nbytes to that, WRITE_CORE_RAM stops there and logs that the rest of the payload was dropped. Both READ_CORE_RAM and READ_CORE_MEMORY additionally refuse requests above 16 KiB: the reply is 3 characters per byte in one UDP datagram, so larger requests could never be delivered anyway, and the cap keeps the size arithmetic well clear of the wrap. READ_CORE_MEMORY already clamped to the descriptor length. Also size the " -1" reply against the allocation instead of sizeof(reply), which was the size of the pointer.
3005 lines
93 KiB
C
3005 lines
93 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
|
|
* Copyright (C) 2011-2017 - Daniel De Matteis
|
|
* Copyright (C) 2021 - David G.F.
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "input/input_driver.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stddef.h>
|
|
#include <locale.h>
|
|
#ifdef HAVE_NETWORKING
|
|
#include <net/net_compat.h>
|
|
#include <net/net_socket.h>
|
|
#endif
|
|
#include <lists/dir_list.h>
|
|
#include <file/file_path.h>
|
|
#include <streams/stdin_stream.h>
|
|
#include <streams/file_stream.h>
|
|
#include <string/stdstring.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_CHEEVOS
|
|
#include "cheevos/cheevos.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_GFX_WIDGETS
|
|
#include "gfx/gfx_widgets.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_MENU
|
|
#include "menu/menu_driver.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_NETWORKING
|
|
#include "network/netplay/netplay.h"
|
|
#endif
|
|
|
|
#include "audio/audio_driver.h"
|
|
#if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL)
|
|
#include "gfx/video_shader_parse.h"
|
|
#endif
|
|
#include "autosave.h"
|
|
#include "command.h"
|
|
#include "core_info.h"
|
|
#include "cheat_manager.h"
|
|
#include "content.h"
|
|
#include "dynamic.h"
|
|
#include "list_special.h"
|
|
#include "paths.h"
|
|
#include "retroarch.h"
|
|
#include "runloop.h"
|
|
#include "verbosity.h"
|
|
#include "version.h"
|
|
#include "version_git.h"
|
|
#include "tasks/task_content.h"
|
|
#include <compat/strl.h>
|
|
#ifdef __MACH__
|
|
#include <TargetConditionals.h>
|
|
#endif
|
|
|
|
#define CMD_BUF_SIZE 4096
|
|
|
|
static void command_post_state_loaded(void)
|
|
{
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_CHEEVOS_HARDCORE_MODE_DISABLED);
|
|
rcheevos_pause_hardcore();
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 0, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
#endif
|
|
#ifdef HAVE_NETWORKING
|
|
netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL);
|
|
#endif
|
|
{
|
|
video_driver_state_t *video_st =
|
|
video_state_get_ptr();
|
|
bool frame_time_counter_auto_reset =
|
|
config_get_ptr()->bools.frame_time_counter_auto_reset;
|
|
if (frame_time_counter_auto_reset)
|
|
video_st->frame_time_count = 0;
|
|
}
|
|
#if defined(HAVE_GFX_WIDGETS) && defined(HAVE_SCREENSHOTS)
|
|
{
|
|
gfx_widget_state_slot_show(dispwidget_get_ptr(), NULL, NULL);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if defined(HAVE_COMMAND)
|
|
|
|
/* Generic command parse utilities */
|
|
|
|
static bool command_get_arg(const char *tok,
|
|
const char **arg, unsigned *index)
|
|
{
|
|
unsigned i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(map); i++)
|
|
{
|
|
if (string_is_equal(tok, map[i].str))
|
|
{
|
|
if (arg)
|
|
*arg = NULL;
|
|
|
|
if (index)
|
|
*index = i;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < ARRAY_SIZE(action_map); i++)
|
|
{
|
|
const char *str = strstr(tok, action_map[i].str);
|
|
if (str == tok)
|
|
{
|
|
const char *argument = str + strlen(action_map[i].str);
|
|
if (!argument)
|
|
return false;
|
|
if (*argument != ' ' && *argument != '\0')
|
|
return false;
|
|
|
|
if (arg)
|
|
*arg = argument + 1;
|
|
|
|
if (index)
|
|
*index = i;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void command_parse_sub_msg(command_t *handle, const char *tok)
|
|
{
|
|
const char *arg = NULL;
|
|
unsigned index = 0;
|
|
|
|
if (command_get_arg(tok, &arg, &index))
|
|
{
|
|
if (arg)
|
|
{
|
|
if (!action_map[index].action(handle, arg))
|
|
RARCH_ERR("[Command] Command \"%s\" failed.\n", arg);
|
|
}
|
|
else
|
|
{
|
|
/* For MENU_TOGGLE, bypass the press-and-release mechanism
|
|
* by directly invoking the command event. This avoids
|
|
* timing issues with runahead (single-instance) where the
|
|
* 1-frame pulse from network commands can be lost. */
|
|
if (map[index].id == RARCH_MENU_TOGGLE)
|
|
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
|
|
else
|
|
handle->state[map[index].id] = true;
|
|
}
|
|
}
|
|
else
|
|
RARCH_WARN(msg_hash_to_str(MSG_UNRECOGNIZED_COMMAND), tok);
|
|
}
|
|
|
|
static void command_parse_msg(command_t *handle, char *buf)
|
|
{
|
|
char *save = NULL;
|
|
const char *tok = strtok_r(buf, "\n", &save);
|
|
unsigned gen = input_driver_command_generation();
|
|
|
|
while (tok)
|
|
{
|
|
command_parse_sub_msg(handle, tok);
|
|
/* The command may have reinitialised the input driver, which
|
|
* owns and has now freed 'handle'. Nothing further may use it. */
|
|
if (input_driver_command_generation() != gen)
|
|
return;
|
|
tok = strtok_r(NULL, "\n", &save);
|
|
}
|
|
}
|
|
|
|
#if defined(HAVE_NETWORK_CMD)
|
|
typedef struct
|
|
{
|
|
/* Network socket FD */
|
|
int net_fd;
|
|
/* Source address for the command received */
|
|
struct sockaddr_storage cmd_source;
|
|
/* Size of the previous structure in use */
|
|
socklen_t cmd_source_len;
|
|
} command_network_t;
|
|
|
|
static void network_command_reply(command_t *cmd,
|
|
const char *s, size_t len)
|
|
{
|
|
command_network_t *netcmd = (command_network_t*)cmd->userptr;
|
|
/* Respond (fire and forget since it's UDP) */
|
|
sendto(netcmd->net_fd, s, len, 0,
|
|
(struct sockaddr*)&netcmd->cmd_source, netcmd->cmd_source_len);
|
|
}
|
|
|
|
static void network_command_free(command_t *handle)
|
|
{
|
|
command_network_t *netcmd = (command_network_t*)handle->userptr;
|
|
|
|
if (netcmd->net_fd >= 0)
|
|
socket_close(netcmd->net_fd);
|
|
|
|
free(netcmd);
|
|
free(handle);
|
|
}
|
|
|
|
static void command_network_poll(command_t *handle)
|
|
{
|
|
ssize_t ret;
|
|
unsigned gen = input_driver_command_generation();
|
|
char buf[2048];
|
|
command_network_t *netcmd = (command_network_t*)handle->userptr;
|
|
|
|
if (netcmd->net_fd < 0)
|
|
return;
|
|
|
|
for (;;)
|
|
{
|
|
netcmd->cmd_source_len = sizeof(netcmd->cmd_source);
|
|
|
|
if ((ret = recvfrom(netcmd->net_fd, buf, sizeof(buf) - 1, 0,
|
|
(struct sockaddr*)&netcmd->cmd_source,
|
|
&netcmd->cmd_source_len)) <= 0)
|
|
return;
|
|
|
|
buf[ret] = '\0';
|
|
|
|
command_parse_msg(handle, buf);
|
|
/* See command_generation: the command may have freed this
|
|
* object. The remaining datagrams wait for the next poll. */
|
|
if (input_driver_command_generation() != gen)
|
|
return;
|
|
}
|
|
}
|
|
|
|
command_t* command_network_new(uint16_t port, 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->destroy = network_command_free;
|
|
|
|
if (!socket_nonblock(netcmd->net_fd))
|
|
goto error;
|
|
|
|
if (!socket_bind(netcmd->net_fd, (void*)res))
|
|
{
|
|
RARCH_ERR("[NetCMD] %s\n",
|
|
msg_hash_to_str(MSG_FAILED_TO_BIND_SOCKET));
|
|
goto error;
|
|
}
|
|
|
|
freeaddrinfo_retro(res);
|
|
return cmd;
|
|
|
|
error:
|
|
if (res)
|
|
freeaddrinfo_retro(res);
|
|
/* The only path that reaches 'error' with fd >= 0 is the
|
|
* socket_nonblock / socket_bind failure after fd was already
|
|
* stored in netcmd->net_fd. Close it so we don't leak the
|
|
* socket file descriptor along with the allocations. */
|
|
if (fd >= 0)
|
|
socket_close(fd);
|
|
free(netcmd);
|
|
free(cmd);
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
#if defined(HAVE_STDIN_CMD)
|
|
typedef struct
|
|
{
|
|
/* Buffer and pointer for stdin reads */
|
|
size_t stdin_buf_ptr;
|
|
char stdin_buf[CMD_BUF_SIZE];
|
|
} command_stdin_t;
|
|
|
|
static void stdin_command_reply(command_t *cmd,
|
|
const char *s, size_t len)
|
|
{
|
|
/* Just write to stdout! */
|
|
fwrite(s, 1, len, stdout);
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void stdin_command_free(command_t *handle)
|
|
{
|
|
free(handle->userptr);
|
|
free(handle);
|
|
}
|
|
|
|
static void command_stdin_poll(command_t *handle)
|
|
{
|
|
command_stdin_t *stdincmd = (command_stdin_t*)handle->userptr;
|
|
ssize_t ret = read_stdin(
|
|
stdincmd->stdin_buf + stdincmd->stdin_buf_ptr,
|
|
CMD_BUF_SIZE - stdincmd->stdin_buf_ptr - 1);
|
|
|
|
if (ret != 0)
|
|
{
|
|
char *last_newline = NULL;
|
|
stdincmd->stdin_buf_ptr += ret;
|
|
|
|
/* Ensure we don't write past buffer bounds */
|
|
if (stdincmd->stdin_buf_ptr >= CMD_BUF_SIZE)
|
|
stdincmd->stdin_buf_ptr = CMD_BUF_SIZE - 1;
|
|
|
|
stdincmd->stdin_buf[stdincmd->stdin_buf_ptr] = '\0';
|
|
|
|
last_newline = strrchr(stdincmd->stdin_buf, '\n');
|
|
|
|
if (!last_newline)
|
|
{
|
|
/* We're receiving bogus data in pipe
|
|
* (no terminating newline), flush out the buffer. */
|
|
if (stdincmd->stdin_buf_ptr + 1 >= CMD_BUF_SIZE)
|
|
{
|
|
stdincmd->stdin_buf_ptr = 0;
|
|
stdincmd->stdin_buf[0] = '\0';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ptrdiff_t msg_len;
|
|
size_t rest;
|
|
char *msg;
|
|
*last_newline++ = '\0';
|
|
msg_len = last_newline - stdincmd->stdin_buf;
|
|
rest = stdincmd->stdin_buf_ptr - msg_len;
|
|
|
|
/* A command can free this object from under us (see
|
|
* command_generation): take the message off the buffer, tidy the
|
|
* buffer, and only then run the message from a private copy,
|
|
* touching nothing of this object afterwards. */
|
|
msg = strdup(stdincmd->stdin_buf);
|
|
memmove(stdincmd->stdin_buf, last_newline, rest);
|
|
stdincmd->stdin_buf_ptr = rest;
|
|
if (msg)
|
|
{
|
|
command_parse_msg(handle, msg);
|
|
free(msg);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
command_t* command_stdin_new(void)
|
|
{
|
|
command_t *cmd;
|
|
command_stdin_t *stdincmd;
|
|
|
|
#if !(defined(_WIN32) || defined(__EMSCRIPTEN__))
|
|
#ifdef HAVE_NETWORKING
|
|
if (!socket_nonblock(STDIN_FILENO))
|
|
return NULL;
|
|
#endif
|
|
#endif
|
|
|
|
/* Allocate in order with per-step failure handling. The earlier
|
|
* form allocated both cmd and stdincmd back-to-back then checked
|
|
* them, which leaked stdincmd on the '!cmd' failure path (the
|
|
* '!cmd' return simply dropped the stdincmd pointer). Also
|
|
* matches the command_uds_new pattern further down. */
|
|
if (!(cmd = (command_t*)calloc(1, sizeof(command_t))))
|
|
return NULL;
|
|
|
|
if (!(stdincmd = (command_stdin_t*)calloc(1, sizeof(command_stdin_t))))
|
|
{
|
|
free(cmd);
|
|
return NULL;
|
|
}
|
|
|
|
cmd->userptr = stdincmd;
|
|
cmd->poll = command_stdin_poll;
|
|
cmd->replier = stdin_command_reply;
|
|
cmd->destroy = stdin_command_free;
|
|
|
|
return cmd;
|
|
}
|
|
#endif
|
|
|
|
#if defined(__EMSCRIPTEN__)
|
|
#include "frontend/drivers/platform_emscripten.h"
|
|
typedef struct
|
|
{
|
|
char command_buf[CMD_BUF_SIZE];
|
|
} command_emscripten_t;
|
|
|
|
static void emscripten_command_reply(command_t *_cmd,
|
|
const char *s, size_t len)
|
|
{
|
|
platform_emscripten_command_reply(s, len);
|
|
}
|
|
|
|
static void emscripten_command_free(command_t *handle)
|
|
{
|
|
free(handle->userptr);
|
|
free(handle);
|
|
}
|
|
|
|
static void command_emscripten_poll(command_t *handle)
|
|
{
|
|
command_emscripten_t *emscriptencmd = (command_emscripten_t*)handle->userptr;
|
|
ptrdiff_t msg_len = platform_emscripten_command_read((char **)(&emscriptencmd->command_buf), CMD_BUF_SIZE);
|
|
if (msg_len != 0)
|
|
command_parse_msg(handle, emscriptencmd->command_buf);
|
|
}
|
|
|
|
command_t* command_emscripten_new(void)
|
|
{
|
|
command_t *cmd;
|
|
command_emscripten_t *emscriptencmd;
|
|
|
|
/* Same sequential-allocation-with-per-step-cleanup pattern as
|
|
* command_stdin_new / command_uds_new. Previously allocated both
|
|
* structs before any NULL check, leaking emscriptencmd on the
|
|
* '!cmd' failure path. */
|
|
if (!(cmd = (command_t*)calloc(1, sizeof(command_t))))
|
|
return NULL;
|
|
|
|
if (!(emscriptencmd = (command_emscripten_t*)calloc(1, sizeof(command_emscripten_t))))
|
|
{
|
|
free(cmd);
|
|
return NULL;
|
|
}
|
|
|
|
cmd->userptr = emscriptencmd;
|
|
cmd->poll = command_emscripten_poll;
|
|
cmd->replier = emscripten_command_reply;
|
|
cmd->destroy = emscripten_command_free;
|
|
|
|
return cmd;
|
|
}
|
|
#endif
|
|
|
|
bool command_get_config_param(command_t *cmd, const char* arg)
|
|
{
|
|
size_t _len;
|
|
char reply[8192];
|
|
#ifdef HAVE_BSV_MOVIE
|
|
char value_dynamic[256];
|
|
#endif
|
|
const char *value = "unsupported";
|
|
settings_t *settings = config_get_ptr();
|
|
bool video_fullscreen = settings->bools.video_fullscreen;
|
|
const char *dir_runtime_log = settings->paths.directory_runtime_log;
|
|
const char *log_dir = settings->paths.log_dir;
|
|
const char *directory_cache = settings->paths.directory_cache;
|
|
const char *directory_system = settings->paths.directory_system;
|
|
const char *path_username = settings->paths.username;
|
|
if (memcmp(arg, "video_fullscreen", sizeof("video_fullscreen")) == 0)
|
|
{
|
|
if (video_fullscreen)
|
|
value = "true";
|
|
else
|
|
value = "false";
|
|
}
|
|
else if (memcmp(arg, "savefile_directory", sizeof("savefile_directory")) == 0)
|
|
value = dir_get_ptr(RARCH_DIR_SAVEFILE);
|
|
else if (memcmp(arg, "savestate_directory", sizeof("savestate_directory")) == 0)
|
|
value = dir_get_ptr(RARCH_DIR_SAVESTATE);
|
|
else if (memcmp(arg, "runtime_log_directory", sizeof("runtime_log_directory")) == 0)
|
|
value = dir_runtime_log;
|
|
else if (memcmp(arg, "log_dir", sizeof("log_dir")) == 0)
|
|
value = log_dir;
|
|
else if (memcmp(arg, "cache_directory", sizeof("cache_directory")) == 0)
|
|
value = directory_cache;
|
|
else if (memcmp(arg, "system_directory", sizeof("system_directory")) == 0)
|
|
value = directory_system;
|
|
else if (memcmp(arg, "netplay_nickname", sizeof("netplay_nickname")) == 0)
|
|
value = path_username;
|
|
#ifdef HAVE_BSV_MOVIE
|
|
else if (memcmp(arg, "active_replay", sizeof("active_replay")) == 0)
|
|
{
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
value = value_dynamic;
|
|
value_dynamic[0] = '\0';
|
|
if (input_st->bsv_movie_state_handle)
|
|
{
|
|
bsv_movie_t *movie = input_st->bsv_movie_state_handle;
|
|
snprintf(value_dynamic, sizeof(value_dynamic), "%lld %u %lld",
|
|
(long long)(movie->identifier),
|
|
input_st->bsv_movie_state.flags,
|
|
(long long)(movie->frame_counter));
|
|
}
|
|
else
|
|
strlcpy_lit(value_dynamic, "0 0 0", sizeof(value_dynamic));
|
|
}
|
|
#endif
|
|
#ifdef HAVE_MENU
|
|
else if (memcmp(arg, "menu_active", sizeof("menu_active")) == 0)
|
|
{
|
|
struct menu_state* menu_st = menu_state_get_ptr();
|
|
if (menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE))
|
|
value = "true";
|
|
else
|
|
value = "false";
|
|
}
|
|
#endif
|
|
#ifdef HAVE_CHEEVOS
|
|
else if (memcmp(arg, "cheevos_enable", sizeof("cheevos_enable")) == 0)
|
|
{
|
|
if (settings->bools.cheevos_enable)
|
|
value = "true";
|
|
else
|
|
value = "false";
|
|
}
|
|
#endif
|
|
/* TODO: query any string */
|
|
_len = strlcpy_lit(reply, "GET_CONFIG_PARAM ", sizeof(reply));
|
|
_len += strlcpy(reply + _len, arg, sizeof(reply) - _len);
|
|
reply[ _len] = ' ';
|
|
reply[++_len] = '\0';
|
|
_len += strlcpy(reply + _len, value, sizeof(reply) - _len);
|
|
cmd->replier(cmd, reply, _len);
|
|
return true;
|
|
}
|
|
|
|
#if defined(HAVE_LAKKA)
|
|
#include <sys/un.h>
|
|
#define MAX_USER_CONNECTIONS 4
|
|
typedef struct
|
|
{
|
|
/* File descriptor for the domain socket */
|
|
int sfd;
|
|
/* Client sockets */
|
|
int userfd[MAX_USER_CONNECTIONS];
|
|
/* Last received user socket */
|
|
int last_fd;
|
|
} command_uds_t;
|
|
|
|
static void uds_command_reply(command_t *cmd,
|
|
const char *s, size_t len)
|
|
{
|
|
command_uds_t *subcmd = (command_uds_t*)cmd->userptr;
|
|
write(subcmd->last_fd, s, len);
|
|
}
|
|
|
|
static void uds_command_free(command_t *handle)
|
|
{
|
|
int i;
|
|
command_uds_t *udscmd = (command_uds_t*)handle->userptr;
|
|
|
|
for (i = 0; i < MAX_USER_CONNECTIONS; i++)
|
|
if (udscmd->userfd[i] >= 0)
|
|
socket_close(udscmd->userfd[i]);
|
|
socket_close(udscmd->sfd);
|
|
|
|
free(handle->userptr);
|
|
free(handle);
|
|
}
|
|
|
|
static void command_uds_poll(command_t *handle)
|
|
{
|
|
unsigned gen = input_driver_command_generation();
|
|
int i;
|
|
int fd;
|
|
ssize_t ret;
|
|
char buf[2048];
|
|
command_uds_t *udscmd = (command_uds_t*)handle->userptr;
|
|
|
|
if (udscmd->sfd < 0)
|
|
return;
|
|
|
|
/* Read data from clients and process commands */
|
|
for (i = 0; i < MAX_USER_CONNECTIONS; i++)
|
|
{
|
|
bool err = false;
|
|
|
|
fd = udscmd->userfd[i];
|
|
if (fd < 0)
|
|
continue;
|
|
|
|
ret = socket_receive_all_nonblocking(fd, &err, buf, sizeof(buf) - 1);
|
|
if (!ret)
|
|
continue;
|
|
|
|
if (!err)
|
|
{
|
|
buf[ret] = '\0';
|
|
udscmd->last_fd = fd;
|
|
|
|
command_parse_msg(handle, buf);
|
|
/* See command_generation: this object may be gone now. */
|
|
if (input_driver_command_generation() != gen)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
socket_close(fd);
|
|
udscmd->userfd[i] = -1;
|
|
}
|
|
}
|
|
|
|
/* Accepts new connections from clients */
|
|
fd = accept(udscmd->sfd, NULL, NULL);
|
|
if (fd >= 0)
|
|
{
|
|
if (socket_nonblock(fd))
|
|
{
|
|
for (i = 0; i < MAX_USER_CONNECTIONS; i++)
|
|
{
|
|
if (udscmd->userfd[i] < 0)
|
|
{
|
|
udscmd->userfd[i] = fd;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
socket_close(fd);
|
|
}
|
|
}
|
|
|
|
command_t* command_uds_new(void)
|
|
{
|
|
int i;
|
|
command_t *cmd;
|
|
command_uds_t *subcmd;
|
|
struct sockaddr_un addr;
|
|
socklen_t addrsz = offsetof(struct sockaddr_un, sun_path) + STRLEN_CONST("retroarch/cmd") + 1;
|
|
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (fd < 0)
|
|
return NULL;
|
|
/* use an abstract socket for simplicity */
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sun_family = AF_UNIX;
|
|
strlcpy_lit(&addr.sun_path[1], "retroarch/cmd", sizeof(addr.sun_path) - 1);
|
|
if ( bind(fd, (struct sockaddr*)&addr, addrsz) < 0
|
|
|| listen(fd, MAX_USER_CONNECTIONS) < 0
|
|
|| !socket_nonblock(fd))
|
|
{
|
|
socket_close(fd);
|
|
return NULL;
|
|
}
|
|
cmd = (command_t*)calloc(1, sizeof(command_t));
|
|
if (!cmd)
|
|
{
|
|
socket_close(fd);
|
|
return NULL;
|
|
}
|
|
subcmd = (command_uds_t*)calloc(1, sizeof(command_uds_t));
|
|
if (!subcmd)
|
|
{
|
|
free(cmd);
|
|
socket_close(fd);
|
|
return NULL;
|
|
}
|
|
subcmd->sfd = fd;
|
|
subcmd->last_fd = -1;
|
|
for (i = 0; i < MAX_USER_CONNECTIONS; i++)
|
|
subcmd->userfd[i] = -1;
|
|
cmd->userptr = subcmd;
|
|
cmd->poll = command_uds_poll;
|
|
cmd->replier = uds_command_reply;
|
|
cmd->destroy = uds_command_free;
|
|
return cmd;
|
|
}
|
|
#endif
|
|
|
|
|
|
/* Routines used to invoke retroarch command ... */
|
|
|
|
#ifdef HAVE_NETWORK_CMD
|
|
static bool command_verify(const char *cmd)
|
|
{
|
|
size_t i;
|
|
if (command_get_arg(cmd, NULL, NULL))
|
|
return true;
|
|
|
|
RARCH_ERR("[NetCMD] Command \"%s\" is not recognized by the program.\n", cmd);
|
|
RARCH_ERR("[NetCMD] \tValid commands:\n");
|
|
for (i = 0; i < ARRAY_SIZE(map); i++)
|
|
RARCH_ERR("[NetCMD] \t\t%s\n", map[i].str);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(action_map); i++)
|
|
RARCH_ERR("[NetCMD] \t\t%s %s\n", action_map[i].str, action_map[i].arg_desc);
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool udp_send_packet(const char *host, uint16_t port, const char *msg)
|
|
{
|
|
char port_buf[6];
|
|
const struct addrinfo *tmp_info;
|
|
struct addrinfo *addr = NULL;
|
|
struct addrinfo hints = {0};
|
|
size_t len = strlen(msg);
|
|
bool ret = false;
|
|
|
|
snprintf(port_buf, sizeof(port_buf), "%hu", (unsigned short)port);
|
|
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
hints.ai_flags = AI_NUMERICSERV;
|
|
|
|
if (getaddrinfo_retro(host, port_buf, &hints, &addr))
|
|
return false;
|
|
if (!addr)
|
|
return false;
|
|
|
|
/* Send to all possible targets. */
|
|
tmp_info = addr;
|
|
|
|
do
|
|
{
|
|
int fd = socket(tmp_info->ai_family,
|
|
tmp_info->ai_socktype, tmp_info->ai_protocol);
|
|
|
|
if (fd < 0)
|
|
continue;
|
|
|
|
if (sendto(fd, msg, len, 0, tmp_info->ai_addr, tmp_info->ai_addrlen) ==
|
|
(ssize_t)len)
|
|
ret = true;
|
|
|
|
socket_close(fd);
|
|
} while ((tmp_info = tmp_info->ai_next));
|
|
|
|
freeaddrinfo_retro(addr);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool command_network_send(const char *cmd_)
|
|
{
|
|
char buf[4096];
|
|
char *ptr;
|
|
const char *cmd = NULL;
|
|
const char *host = NULL;
|
|
const char *port_str = NULL;
|
|
uint16_t port = DEFAULT_NETWORK_CMD_PORT;
|
|
size_t len;
|
|
|
|
if (!cmd_ || !*cmd_)
|
|
return false;
|
|
|
|
len = strlen(cmd_);
|
|
if (len >= sizeof(buf))
|
|
return false;
|
|
|
|
if (!network_init())
|
|
return false;
|
|
|
|
memcpy(buf, cmd_, len + 1);
|
|
|
|
cmd = buf;
|
|
ptr = strchr(buf, ';');
|
|
if (ptr)
|
|
{
|
|
*ptr++ = '\0';
|
|
host = ptr;
|
|
ptr = strchr(ptr, ';');
|
|
if (ptr)
|
|
{
|
|
*ptr++ = '\0';
|
|
port_str = ptr;
|
|
}
|
|
}
|
|
|
|
if (!cmd || !*cmd)
|
|
return false;
|
|
|
|
if (!host || !*host)
|
|
{
|
|
#ifdef _WIN32
|
|
host = "127.0.0.1";
|
|
#else
|
|
host = "localhost";
|
|
#endif
|
|
}
|
|
|
|
if (port_str && *port_str)
|
|
{
|
|
unsigned long val = strtoul(port_str, NULL, 0);
|
|
if (val > 0 && val <= 0xFFFF)
|
|
port = (uint16_t)val;
|
|
}
|
|
|
|
RARCH_LOG("[NetCMD] %s: \"%s\" to %s:%hu.\n",
|
|
msg_hash_to_str(MSG_SENDING_COMMAND),
|
|
cmd, host, (unsigned short)port);
|
|
|
|
if (!command_verify(cmd))
|
|
return false;
|
|
|
|
return udp_send_packet(host, port, cmd);
|
|
}
|
|
#endif
|
|
|
|
bool command_show_osd_msg(command_t *cmd, const char* arg)
|
|
{
|
|
runloop_msg_queue_push(arg, strlen(arg), 1, 180, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool command_load_state_slot(command_t *cmd, const char *arg)
|
|
{
|
|
char state_path[PATH_MAX_LENGTH] = "";
|
|
size_t _len = 0;
|
|
char reply[128] = "";
|
|
unsigned int slot = (unsigned int)strtoul(arg, NULL, 10);
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
_len = strlcpy_lit(reply, "LOAD_STATE_SLOT ", sizeof(reply));
|
|
_len += snprintf(reply + _len, sizeof(reply) - _len, "%d", slot);
|
|
runloop_get_savestate_path(state_path, sizeof(state_path), slot);
|
|
/* For LOADING, an existing state file outranks metadata and
|
|
* save-capability probes: core_serialize_size() measures whether the
|
|
* core can SAVE right now (0 at e.g. a game's own main menu), which
|
|
* says nothing about whether it can restore. Let the load task and
|
|
* retro_unserialize() arbitrate. */
|
|
if (!savestates_enabled)
|
|
savestates_enabled = path_is_valid(state_path);
|
|
if (savestates_enabled)
|
|
{
|
|
if ((ret = content_load_state(state_path, false, false)))
|
|
command_post_state_loaded();
|
|
}
|
|
else
|
|
ret = false;
|
|
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
bool command_save_state_slot(command_t* cmd, const char* arg)
|
|
{
|
|
char state_path[PATH_MAX_LENGTH] = "";
|
|
size_t _len = 0;
|
|
char reply[128] = "";
|
|
unsigned int slot = (unsigned int)strtoul(arg, NULL, 10);
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
_len = strlcpy_lit(reply, "SAVE_STATE_SLOT ", sizeof(reply));
|
|
_len += snprintf(reply + _len, sizeof(reply) - _len, "%d", slot);
|
|
if (savestates_enabled)
|
|
{
|
|
size_t info_size;
|
|
runloop_get_savestate_path(state_path, sizeof(state_path), slot);
|
|
|
|
info_size = core_serialize_size();
|
|
savestates_enabled = (info_size > 0);
|
|
}
|
|
if (savestates_enabled)
|
|
ret = content_save_state(state_path, true);
|
|
else
|
|
ret = false;
|
|
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
bool command_play_replay_slot(command_t *cmd, const char *arg)
|
|
{
|
|
#ifdef HAVE_BSV_MOVIE
|
|
char replay_path[16384];
|
|
char reply[128] = "";
|
|
unsigned int slot = (unsigned int)strtoul(arg, NULL, 10);
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
replay_path[0] = '\0';
|
|
if (savestates_enabled)
|
|
{
|
|
size_t info_size;
|
|
runloop_get_replay_path(replay_path, sizeof(replay_path), slot);
|
|
|
|
info_size = core_serialize_size();
|
|
savestates_enabled = (info_size > 0);
|
|
}
|
|
if (savestates_enabled)
|
|
{
|
|
ret = movie_start_playback(input_state_get_ptr(), replay_path);
|
|
if (ret)
|
|
{
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
/* The reply carries the replay handle, which the movie
|
|
* task's callback installs, so this still waits - but only
|
|
* for that task. A NULL condition means "until the queue
|
|
* is empty", which made a network command block on every
|
|
* unrelated scan or download in flight. */
|
|
task_queue_wait(movie_playback_start_in_progress, NULL);
|
|
if (input_st->bsv_movie_state_next_handle)
|
|
snprintf(reply, sizeof(reply) - 1, "PLAY_REPLAY_SLOT %lld", (long long)(input_st->bsv_movie_state_next_handle->identifier));
|
|
else
|
|
snprintf(reply, sizeof(reply) - 1, "PLAY_REPLAY_SLOT 0");
|
|
command_post_state_loaded();
|
|
}
|
|
}
|
|
else
|
|
ret = false;
|
|
|
|
cmd->replier(cmd, reply, strlen(reply));
|
|
return ret;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool command_seek_replay(command_t *cmd, const char *arg)
|
|
{
|
|
#ifdef HAVE_BSV_MOVIE
|
|
char reply[32];
|
|
char *endptr = NULL;
|
|
size_t _len;
|
|
bool ret = true;
|
|
int64_t frame = arg ? (int64_t)strtoll(arg, &endptr, 10) : 0;
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
/* strtoll always writes a valid pointer, so the end pointer is
|
|
* never NULL - an empty or non-numeric argument shows up as no
|
|
* characters consumed. */
|
|
if (!arg || endptr == arg)
|
|
ret = false;
|
|
if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_PLAYBACK | BSV_FLAG_MOVIE_RECORDING)))
|
|
ret = false;
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
ret = false;
|
|
#endif
|
|
if (ret)
|
|
ret = movie_seek_to_frame(input_st, frame);
|
|
if (ret)
|
|
{
|
|
_len = strlcpy_lit(reply, "OK ", sizeof(reply));
|
|
_len += snprintf(reply+_len, sizeof(reply)-_len,
|
|
"%" PRId64, input_st->bsv_movie_state.seek_target_frame);
|
|
}
|
|
else
|
|
_len = strlcpy_lit(reply, "NO", sizeof(reply));
|
|
reply[_len] = '\n';
|
|
reply[++_len] = '\0';
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
#else
|
|
cmd->replier(cmd, "NO\n", 4);
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool command_save_savefiles(command_t *cmd, const char* arg)
|
|
{
|
|
char reply[4];
|
|
bool ret;
|
|
size_t _len = strlcpy_lit(reply, "OK", sizeof(reply));
|
|
reply[ _len] = '\n';
|
|
reply[++_len] = '\0';
|
|
/* In the future, this should probably send each saved file path
|
|
to the replier. */
|
|
ret = command_event(CMD_EVENT_SAVE_FILES, NULL);
|
|
if (!ret)
|
|
strlcpy_lit(reply, "NO", sizeof(reply));
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
bool command_load_savefiles(command_t *cmd, const char* arg)
|
|
{
|
|
char reply[4];
|
|
bool ret;
|
|
size_t _len = strlcpy_lit(reply, "OK", sizeof(reply));
|
|
reply[ _len] = '\n';
|
|
reply[++_len] = '\0';
|
|
ret = command_event(CMD_EVENT_LOAD_FILES, NULL);
|
|
if (!ret)
|
|
strlcpy_lit(reply, "NO", sizeof(reply));
|
|
cmd->replier(cmd, reply, _len);
|
|
return ret;
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
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)
|
|
{
|
|
cmd->replier(cmd, "READ_CORE_RAM ERROR: OUT OF MEMORY\n", 34);
|
|
return true;
|
|
}
|
|
|
|
reply[0] = '\0';
|
|
reply_at = reply + snprintf(
|
|
reply, alloc_size - 1, "READ_CORE_RAM" " %x", addr);
|
|
|
|
if (data && 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
|
|
{
|
|
strlcpy_lit(reply_at, " -1\n", alloc_size - (size_t)(reply_at - reply));
|
|
_len = reply_at + STRLEN_CONST(" -1\n") - reply;
|
|
}
|
|
cmd->replier(cmd, reply, _len);
|
|
free(reply);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool command_write_ram(command_t *cmd, const char *arg)
|
|
{
|
|
unsigned int addr = (unsigned int)strtoul(arg, (char**)&arg, 16);
|
|
unsigned int avail = 0;
|
|
uint8_t *data = (uint8_t *)rcheevos_patch_address_avail(addr, &avail);
|
|
|
|
if (!data)
|
|
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);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
bool command_version(command_t *cmd, const char* arg)
|
|
{
|
|
char reply[256];
|
|
size_t _len = strlcpy(reply, PACKAGE_VERSION, sizeof(reply));
|
|
reply[ _len] = '\n';
|
|
reply[++_len] = '\0';
|
|
cmd->replier(cmd, reply, _len);
|
|
return true;
|
|
}
|
|
|
|
/* LOAD_CORE <core path>
|
|
*
|
|
* Mirrors selecting a core in the menu's core list / file browser,
|
|
* action_ok_load_core() -> generic_action_ok()'s ACTION_OK_LOAD_CORE
|
|
* branch. Same task_push_load_new_core() call with the same arguments;
|
|
* the result is propagated rather than discarded so a bad core path is
|
|
* reported as a failed command instead of a silent no-op. */
|
|
bool command_load_core(command_t *cmd, const char* arg)
|
|
{
|
|
content_ctx_info_t content_info = {0};
|
|
|
|
if (!arg || !*arg)
|
|
return false;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
{
|
|
char exp[PATH_MAX_LENGTH];
|
|
fill_pathname_expand_special(exp, arg, sizeof(exp));
|
|
return task_push_load_new_core(exp, NULL,
|
|
&content_info, CORE_TYPE_PLAIN, NULL, NULL);
|
|
}
|
|
#else
|
|
return task_push_load_new_core(arg, NULL,
|
|
&content_info, CORE_TYPE_PLAIN, NULL, NULL);
|
|
#endif
|
|
}
|
|
|
|
/* START_CORE
|
|
*
|
|
* Mirrors the Main Menu "Start Core" entry, action_ok_start_core():
|
|
* clears the content path and starts the currently loaded core without
|
|
* content. The list_cache() call that entry makes first is menu
|
|
* animation bookkeeping and has no equivalent here. */
|
|
bool command_start_core(command_t *cmd, const char* arg)
|
|
{
|
|
content_ctx_info_t content_info = {0};
|
|
|
|
path_clear(RARCH_PATH_BASENAME);
|
|
|
|
return task_push_start_current_core(&content_info);
|
|
}
|
|
|
|
/* LOAD_CONTENT <core path>|<content path>
|
|
*
|
|
* The separator is '|' rather than a space because both operands are
|
|
* filesystem paths and may legitimately contain spaces. Routed through
|
|
* the companion UI entry point so that the command interface takes the
|
|
* same content load path as any other frontend-external requester,
|
|
* rather than duplicating the menu-only variant. */
|
|
bool command_load_content(command_t *cmd, const char* arg)
|
|
{
|
|
char core_path[PATH_MAX_LENGTH];
|
|
content_ctx_info_t content_info = {0};
|
|
const char *sep = NULL;
|
|
size_t _len = 0;
|
|
|
|
if (!arg || !*arg)
|
|
return false;
|
|
if (!(sep = strchr(arg, '|')))
|
|
return false;
|
|
|
|
_len = (size_t)(sep - arg);
|
|
if (_len == 0 || _len >= sizeof(core_path))
|
|
return false;
|
|
if (!*(sep + 1))
|
|
return false;
|
|
|
|
memcpy(core_path, arg, _len);
|
|
core_path[_len] = '\0';
|
|
|
|
#if TARGET_OS_IPHONE
|
|
{
|
|
char exp[PATH_MAX_LENGTH];
|
|
fill_pathname_expand_special(exp, sep + 1, sizeof(exp));
|
|
return task_push_load_content_with_new_core_from_companion_ui(
|
|
core_path, exp, NULL, NULL, NULL, &content_info, NULL, NULL);
|
|
}
|
|
#else
|
|
return task_push_load_content_with_new_core_from_companion_ui(
|
|
core_path, sep + 1, NULL, NULL, NULL, &content_info, NULL, NULL);
|
|
#endif
|
|
}
|
|
|
|
/* CLOSE_CONTENT
|
|
*
|
|
* On HAVE_MENU builds this is CMD_EVENT_CLOSE_CONTENT, the same event the
|
|
* Quick Menu "Close Content" entry issues.
|
|
*
|
|
* On builds without a menu, CMD_EVENT_CLOSE_CONTENT is defined as
|
|
* CMD_EVENT_QUIT -- reasonable for a hotkey on a frontend that has
|
|
* nowhere to return to, but wrong here: a command interface consumer
|
|
* asking to close content is not asking to terminate the process, and
|
|
* has no way to discover that the two are the same on this build. Use
|
|
* CMD_EVENT_UNLOAD_CORE instead, which unloads the core and starts the
|
|
* dummy core. That is as close to "content closed" as a menuless build
|
|
* gets, and it leaves the process alive to accept further commands.
|
|
*
|
|
* This was previously a map[] entry driving RARCH_CLOSE_CONTENT_KEY,
|
|
* which is not what the menu entry does either: the hotkey path in
|
|
* runloop_iterate() also applies the 'confirm_close' double press timer,
|
|
* and nothing external can produce the second press inside the window,
|
|
* so with that setting enabled the command was silently swallowed. The
|
|
* menu entry's confirmation is a dialog rather than a timer and is
|
|
* equally unreachable over the command interface, so neither form of
|
|
* confirmation applies on this path. */
|
|
bool command_close_content(command_t *cmd, const char* arg)
|
|
{
|
|
#ifdef HAVE_MENU
|
|
return command_event(CMD_EVENT_CLOSE_CONTENT, NULL);
|
|
#else
|
|
return command_event(CMD_EVENT_UNLOAD_CORE, NULL);
|
|
#endif
|
|
}
|
|
|
|
/* UNLOAD_CORE
|
|
*
|
|
* CMD_EVENT_UNLOAD_CORE releases the core and starts the dummy core, and
|
|
* is not menu dependent. The Main Menu "Unload Core" entry issues the
|
|
* same event and then clears the last core path; that clear is a
|
|
* frontend path operation rather than a menu one, so it belongs here
|
|
* too. Only the entry refresh afterwards is menu state. */
|
|
bool command_unload_core(command_t *cmd, const char* arg)
|
|
{
|
|
if (!command_event(CMD_EVENT_UNLOAD_CORE, NULL))
|
|
return false;
|
|
|
|
path_clear(RARCH_PATH_CORE_LAST);
|
|
|
|
#ifdef HAVE_MENU
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
if (menu_st)
|
|
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH
|
|
| MENU_ST_FLAG_PREVENT_POPULATE;
|
|
}
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
/* VIDEO_REINIT / AUDIO_REINIT / DRIVERS_REINIT
|
|
*
|
|
* Driver reinit, split by scope. None has a menu equivalent; they exist
|
|
* so that a driver teardown and rebuild can be triggered on its own,
|
|
* rather than only as a side effect of loading content.
|
|
*
|
|
* CMD_EVENT_REINIT takes an optional driver mask through its data
|
|
* pointer and falls back to DRIVERS_CMD_ALL when passed NULL, so
|
|
* VIDEO_REINIT passes DRIVER_VIDEO_MASK explicitly rather than relying on
|
|
* the default -- otherwise it would tear down audio, input, MIDI and the
|
|
* rest as well, and would not be a video reinit at all. A video-only
|
|
* mask through video_driver_reinit() is what the CRT switch path already
|
|
* does. DRIVERS_REINIT keeps the all-drivers behaviour under a name that
|
|
* says so. */
|
|
bool command_video_reinit(command_t *cmd, const char* arg)
|
|
{
|
|
int flags = DRIVER_VIDEO_MASK;
|
|
command_event(CMD_EVENT_REINIT, &flags);
|
|
return true;
|
|
}
|
|
|
|
bool command_audio_reinit(command_t *cmd, const char* arg)
|
|
{
|
|
return command_event(CMD_EVENT_AUDIO_REINIT, NULL);
|
|
}
|
|
|
|
bool command_drivers_reinit(command_t *cmd, const char* arg)
|
|
{
|
|
command_event(CMD_EVENT_REINIT, NULL);
|
|
return true;
|
|
}
|
|
|
|
static const rarch_memory_descriptor_t* command_memory_get_descriptor(const rarch_memory_map_t* mmap, unsigned address, size_t* offset)
|
|
{
|
|
const rarch_memory_descriptor_t* desc = mmap->descriptors;
|
|
const rarch_memory_descriptor_t* end = desc + mmap->num_descriptors;
|
|
|
|
for (; desc < end; desc++)
|
|
{
|
|
if (desc->core.select == 0)
|
|
{
|
|
/* if select is 0, attempt to explicitly match the address */
|
|
if (address >= desc->core.start && address < desc->core.start + desc->core.len)
|
|
{
|
|
*offset = address - desc->core.start;
|
|
return desc;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* otherwise, attempt to match the address by matching the select bits */
|
|
if (((desc->core.start ^ address) & desc->core.select) == 0)
|
|
{
|
|
/* adjust the address to the start of the descriptor */
|
|
unsigned desc_offset = address - (unsigned)desc->core.start;
|
|
|
|
/* address is unsigned. we only need that much of the disconnect mask */
|
|
unsigned mask = (unsigned)desc->core.disconnect;
|
|
|
|
/* this magic logic is copied from mmap_reduce. it removes any bits from
|
|
* address that are non-zero in the disconnect field. bits above the
|
|
* removed bits are shifted down to fill the gap. */
|
|
while (mask)
|
|
{
|
|
const unsigned tmp = (mask - 1) & ~mask;
|
|
desc_offset = (desc_offset & tmp) | ((desc_offset >> 1) & ~tmp);
|
|
mask = (mask & (mask - 1)) >> 1;
|
|
}
|
|
|
|
/* we've calculated the actual offset of the data within the descriptor */
|
|
*offset = desc_offset;
|
|
|
|
/* sanity check - make sure the descriptor is large enough to hold the target address */
|
|
if (desc_offset < desc->core.len)
|
|
return desc;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static uint8_t *command_memory_get_pointer(
|
|
const rarch_system_info_t* sys_info,
|
|
unsigned address, unsigned int* max_bytes,
|
|
int for_write, char *s, size_t len)
|
|
{
|
|
if (!sys_info || sys_info->mmaps.num_descriptors == 0)
|
|
strlcpy_lit(s, " -1 no memory map defined\n", len);
|
|
else
|
|
{
|
|
size_t offset;
|
|
const rarch_memory_descriptor_t* desc = command_memory_get_descriptor(&sys_info->mmaps, address, &offset);
|
|
if (!desc)
|
|
strlcpy_lit(s, " -1 no descriptor for address\n", len);
|
|
else if (!desc->core.ptr)
|
|
strlcpy_lit(s, " -1 no data for descriptor\n", len);
|
|
else if (for_write && (desc->core.flags & RETRO_MEMDESC_CONST))
|
|
strlcpy_lit(s, " -1 descriptor data is readonly\n", len);
|
|
else
|
|
{
|
|
*max_bytes = (unsigned int)(desc->core.len - offset);
|
|
return (uint8_t*)desc->core.ptr + desc->core.offset + offset;
|
|
}
|
|
}
|
|
|
|
*max_bytes = 0;
|
|
return NULL;
|
|
}
|
|
|
|
bool command_get_status(command_t *cmd, const char* arg)
|
|
{
|
|
size_t _len;
|
|
char reply[4096];
|
|
uint8_t flags;
|
|
|
|
if (!cmd)
|
|
return false;
|
|
|
|
if (!cmd->replier)
|
|
return false;
|
|
|
|
flags = content_get_flags();
|
|
|
|
if (flags & CONTENT_ST_FLAG_IS_INITED)
|
|
{
|
|
/* add some content info */
|
|
core_info_t *core_info = NULL;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *basename_path = NULL;
|
|
|
|
if (!runloop_st)
|
|
{
|
|
_len = strlcpy_lit(reply, "GET_STATUS ERROR", sizeof(reply));
|
|
cmd->replier(cmd, reply, _len);
|
|
return false;
|
|
}
|
|
|
|
core_info_get_current_core(&core_info);
|
|
|
|
_len = 0;
|
|
strlcpy_append(reply, sizeof(reply), &_len, "GET_STATUS ");
|
|
|
|
if (runloop_st->flags & RUNLOOP_FLAG_PAUSED)
|
|
strlcpy_append(reply, sizeof(reply), &_len, "PAUSED");
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "PLAYING");
|
|
|
|
strlcpy_append(reply, sizeof(reply), &_len, " ");
|
|
|
|
if (core_info && core_info->system_id)
|
|
strlcpy_append(reply, sizeof(reply), &_len, core_info->system_id);
|
|
else if (runloop_st->system.info.library_name)
|
|
strlcpy_append(reply, sizeof(reply), &_len,
|
|
runloop_st->system.info.library_name);
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN");
|
|
|
|
strlcpy_append(reply, sizeof(reply), &_len, ",");
|
|
|
|
basename_path = path_get(RARCH_PATH_BASENAME);
|
|
if (basename_path)
|
|
{
|
|
const char *basename = path_basename(basename_path);
|
|
if (basename)
|
|
strlcpy_append(reply, sizeof(reply), &_len, basename);
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN");
|
|
}
|
|
else
|
|
strlcpy_append(reply, sizeof(reply), &_len, "UNKNOWN");
|
|
|
|
strlcpy_append(reply, sizeof(reply), &_len, "\n");
|
|
}
|
|
else
|
|
_len = strlcpy_lit(reply, "GET_STATUS CONTENTLESS", sizeof(reply));
|
|
|
|
cmd->replier(cmd, reply, _len);
|
|
return true;
|
|
}
|
|
|
|
bool command_read_memory(command_t *cmd, const char *arg)
|
|
{
|
|
unsigned i;
|
|
char* reply = NULL;
|
|
char* reply_at = NULL;
|
|
const uint8_t* data = NULL;
|
|
unsigned int nbytes = 0;
|
|
unsigned int alloc_size = 0;
|
|
unsigned int address = -1;
|
|
size_t _len = 0;
|
|
unsigned int max_bytes = 0;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const rarch_system_info_t* sys_info= &runloop_st->system;
|
|
|
|
{
|
|
char *end = NULL;
|
|
address = (unsigned int)strtoul(arg, &end, 16);
|
|
if (!(end && *end == ' '))
|
|
return false;
|
|
nbytes = (unsigned int)strtoul(end + 1, NULL, 10);
|
|
if (nbytes == 0 || nbytes > 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)
|
|
{
|
|
cmd->replier(cmd, "READ_CORE_MEMORY ERROR: OUT OF MEMORY\n", 37);
|
|
return true;
|
|
}
|
|
|
|
reply_at = reply + snprintf(reply, alloc_size - 1, "READ_CORE_MEMORY %x", address);
|
|
|
|
if ((data = command_memory_get_pointer(
|
|
sys_info, address, &max_bytes,
|
|
0, reply_at, alloc_size - strlen(reply))))
|
|
{
|
|
if (nbytes > max_bytes)
|
|
nbytes = max_bytes;
|
|
|
|
for (i = 0; i < nbytes; i++)
|
|
snprintf(reply_at + 3 * i, 4, " %02X", data[i]);
|
|
|
|
reply_at[3 * nbytes] = '\n';
|
|
_len = reply_at + 3 * nbytes + 1 - reply;
|
|
}
|
|
else
|
|
_len = strlen(reply);
|
|
|
|
cmd->replier(cmd, reply, _len);
|
|
free(reply);
|
|
return true;
|
|
}
|
|
|
|
bool command_write_memory(command_t *cmd, const char *arg)
|
|
{
|
|
unsigned int address = (unsigned int)strtoul(arg, (char**)&arg, 16);
|
|
unsigned int max_bytes = 0;
|
|
char reply[128] = "";
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const rarch_system_info_t
|
|
*sys_info = &runloop_st->system;
|
|
char *reply_at = reply + snprintf(reply, sizeof(reply) - 1, "WRITE_CORE_MEMORY %x", address);
|
|
uint8_t *data = command_memory_get_pointer(sys_info, address, &max_bytes, 1,
|
|
reply_at, sizeof(reply) - strlen(reply) - 1);
|
|
|
|
if (data)
|
|
{
|
|
uint8_t* start = data;
|
|
while (*arg && max_bytes > 0)
|
|
{
|
|
--max_bytes;
|
|
*data = strtoul(arg, (char**)&arg, 16);
|
|
data++;
|
|
}
|
|
|
|
snprintf(reply_at, sizeof(reply) - strlen(reply) - 1,
|
|
" %u\n", (unsigned)(data - start));
|
|
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
{
|
|
RARCH_LOG("[Command] Achievements hardcore mode disabled by WRITE_CORE_MEMORY.\n");
|
|
rcheevos_pause_hardcore();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
cmd->replier(cmd, reply, strlen(reply));
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void command_event_set_volume(
|
|
settings_t *settings,
|
|
float gain,
|
|
bool widgets_active,
|
|
bool audio_driver_mute_enable)
|
|
{
|
|
size_t _len;
|
|
char msg[128];
|
|
float new_volume = settings->floats.audio_volume + gain;
|
|
new_volume = MAX(new_volume, -80.0f);
|
|
new_volume = MIN(new_volume, 12.0f);
|
|
configuration_set_float(settings, settings->floats.audio_volume, new_volume);
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_AUDIO_VOLUME),
|
|
sizeof(msg));
|
|
_len += strlcpy_lit(msg + _len, ": ", sizeof(msg) - _len);
|
|
_len += snprintf(msg + _len, sizeof(msg) - _len, "%.1f",
|
|
new_volume);
|
|
_len += strlcpy_lit(msg + _len, " dB", sizeof(msg) - _len);
|
|
|
|
#if defined(HAVE_GFX_WIDGETS)
|
|
if (widgets_active)
|
|
gfx_widget_volume_update_and_show(new_volume,
|
|
audio_driver_mute_enable);
|
|
else
|
|
#endif
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
RARCH_LOG("[Audio] %s.\n", msg);
|
|
|
|
audio_set_float(AUDIO_ACTION_VOLUME_GAIN, new_volume);
|
|
}
|
|
|
|
/**
|
|
* event_set_mixer_volume:
|
|
* @gain : amount of gain to be applied to current volume level.
|
|
*
|
|
* Adjusts the current audio volume level.
|
|
*
|
|
**/
|
|
void command_event_set_mixer_volume(
|
|
settings_t *settings,
|
|
float gain)
|
|
{
|
|
size_t _len;
|
|
char msg[128];
|
|
float new_volume = settings->floats.audio_mixer_volume + gain;
|
|
new_volume = MAX(new_volume, -80.0f);
|
|
new_volume = MIN(new_volume, 12.0f);
|
|
configuration_set_float(settings, settings->floats.audio_mixer_volume, new_volume);
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_AUDIO_VOLUME),
|
|
sizeof(msg));
|
|
_len += strlcpy_lit(msg + _len, ": ", sizeof(msg) - _len);
|
|
_len += snprintf(msg + _len, sizeof(msg) - _len, "%.1f",
|
|
new_volume);
|
|
_len += strlcpy_lit(msg + _len, " dB", sizeof(msg) - _len);
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
RARCH_LOG("[Audio] %s.\n", msg);
|
|
|
|
audio_set_float(AUDIO_ACTION_VOLUME_GAIN, new_volume);
|
|
}
|
|
|
|
void command_event_init_controllers(rarch_system_info_t *sys_info,
|
|
settings_t *settings, unsigned num_active_users)
|
|
{
|
|
unsigned port;
|
|
unsigned num_core_ports = sys_info->ports.size;
|
|
|
|
for (port = 0; port < num_core_ports; port++)
|
|
{
|
|
size_t i;
|
|
retro_ctx_controller_info_t pad;
|
|
unsigned device = RETRO_DEVICE_NONE;
|
|
const struct retro_controller_description *desc = NULL;
|
|
|
|
/* Check whether current core port is mapped
|
|
* to an input device
|
|
* > If is not, leave 'device' set to
|
|
* 'RETRO_DEVICE_NONE'
|
|
* > For example: if input ports 0 and 1 are
|
|
* mapped to core port 0, core port 1 will
|
|
* be unmapped and should be disabled */
|
|
for (i = 0; i < num_active_users; i++)
|
|
{
|
|
if (i >= MAX_USERS)
|
|
break;
|
|
|
|
if (port == settings->uints.input_remap_ports[i])
|
|
{
|
|
device = input_config_get_device(port);
|
|
break;
|
|
}
|
|
}
|
|
|
|
desc = libretro_find_controller_description(
|
|
&sys_info->ports.data[port], device);
|
|
|
|
if (desc && !desc->desc)
|
|
{
|
|
/* If we're trying to connect a completely unknown device,
|
|
* revert back to JOYPAD. */
|
|
if (device != RETRO_DEVICE_JOYPAD && device != RETRO_DEVICE_NONE)
|
|
{
|
|
/* Do not fix device,
|
|
* because any use of dummy core will reset this,
|
|
* which is not a good idea. */
|
|
RARCH_WARN("[Input] Input device ID %u is unknown to this "
|
|
"libretro implementation. Using RETRO_DEVICE_JOYPAD.\n",
|
|
device);
|
|
device = RETRO_DEVICE_JOYPAD;
|
|
}
|
|
}
|
|
|
|
pad.device = device;
|
|
pad.port = port;
|
|
core_set_controller_port_device(&pad);
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_CONFIGFILE
|
|
static size_t command_event_save_config(const char *config_path,
|
|
char *s, size_t len)
|
|
{
|
|
size_t _len = 0;
|
|
bool path_exists = *config_path;
|
|
const char *str = path_exists ? config_path :
|
|
path_get(RARCH_PATH_CONFIG);
|
|
|
|
/* Workaround for libdecor 0.2.0 setting unwanted locale */
|
|
#if defined(HAVE_WAYLAND) && defined(HAVE_DYNAMIC)
|
|
setlocale(LC_NUMERIC,"C");
|
|
#endif
|
|
if (path_exists && config_save_file(config_path))
|
|
{
|
|
#if TARGET_OS_IPHONE
|
|
char tmp[PATH_MAX_LENGTH] = {0};
|
|
fill_pathname_abbreviate_special(tmp, config_path, sizeof(tmp));
|
|
_len = snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_SAVED_NEW_CONFIG_TO),
|
|
tmp);
|
|
#else
|
|
_len = snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_SAVED_NEW_CONFIG_TO),
|
|
config_path);
|
|
#endif
|
|
RARCH_LOG("[Config] %s\n", s);
|
|
return _len;
|
|
}
|
|
|
|
if (str && *str)
|
|
{
|
|
_len = snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_FAILED_SAVING_CONFIG_TO),
|
|
str);
|
|
RARCH_ERR("[Config] %s\n", s);
|
|
}
|
|
|
|
return _len;
|
|
}
|
|
#endif
|
|
|
|
static size_t command_event_undo_save_state(char *s, size_t len)
|
|
{
|
|
if (content_undo_save_buf_is_empty())
|
|
{
|
|
enum msg_hash_enums msg = content_undo_save_disabled()
|
|
? MSG_CORE_DOES_NOT_SUPPORT_SAVESTATE_UNDO
|
|
: MSG_NO_SAVE_STATE_HAS_BEEN_OVERWRITTEN_YET;
|
|
return strlcpy(s, msg_hash_to_str(msg), len);
|
|
}
|
|
if (!content_undo_save_state())
|
|
return strlcpy(s,
|
|
msg_hash_to_str(MSG_FAILED_TO_UNDO_SAVE_STATE), len);
|
|
return strlcpy(s,
|
|
msg_hash_to_str(MSG_UNDOING_SAVE_STATE), len);
|
|
}
|
|
|
|
static size_t command_event_undo_load_state(char *s, size_t len)
|
|
{
|
|
if (content_undo_load_buf_is_empty())
|
|
{
|
|
enum msg_hash_enums msg = content_undo_save_disabled()
|
|
? MSG_CORE_DOES_NOT_SUPPORT_SAVESTATE_UNDO
|
|
: MSG_NO_STATE_HAS_BEEN_LOADED_YET;
|
|
return strlcpy(s, msg_hash_to_str(msg), len);
|
|
}
|
|
if (!content_undo_load_state())
|
|
return snprintf(s, len, "%s \"%s\".",
|
|
msg_hash_to_str(MSG_FAILED_TO_UNDO_LOAD_STATE),
|
|
"RAM");
|
|
#ifdef HAVE_NETWORKING
|
|
netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL);
|
|
#endif
|
|
return strlcpy(s,
|
|
msg_hash_to_str(MSG_UNDID_LOAD_STATE), len);
|
|
}
|
|
|
|
bool command_event_resize_windowed_scale(settings_t *settings,
|
|
unsigned window_scale)
|
|
{
|
|
unsigned idx = 0;
|
|
bool video_fullscreen = settings->bools.video_fullscreen;
|
|
|
|
if (window_scale == 0)
|
|
return false;
|
|
|
|
configuration_set_uint(settings, settings->uints.video_scale, window_scale);
|
|
|
|
if (!video_fullscreen)
|
|
command_event(CMD_EVENT_REINIT, NULL);
|
|
|
|
retroarch_ctl(RARCH_CTL_SET_WINDOWED_SCALE, &idx);
|
|
|
|
return true;
|
|
}
|
|
|
|
size_t command_event_save_auto_state(void)
|
|
{
|
|
size_t _len;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_savestate = runloop_st->name.savestate;
|
|
char savestate_name_auto[PATH_MAX_LENGTH];
|
|
const char *a = NULL;
|
|
|
|
if (!core_info_current_supports_savestate())
|
|
return 0;
|
|
a = path_basename(path_get(RARCH_PATH_BASENAME));
|
|
if (!a || !*a)
|
|
return 0;
|
|
_len = strlcpy(savestate_name_auto, name_savestate,
|
|
sizeof(savestate_name_auto));
|
|
_len += strlcpy_lit(savestate_name_auto + _len, ".auto",
|
|
sizeof(savestate_name_auto) - _len);
|
|
if (content_auto_save_state((const char*)savestate_name_auto))
|
|
RARCH_LOG("[State] %s \"%s\" %s.\n",
|
|
msg_hash_to_str(MSG_AUTO_SAVE_STATE_TO),
|
|
savestate_name_auto, "succeeded");
|
|
else
|
|
RARCH_LOG("[State] %s \"%s\" %s.\n",
|
|
msg_hash_to_str(MSG_AUTO_SAVE_STATE_TO),
|
|
savestate_name_auto, "failed");
|
|
return _len;
|
|
}
|
|
|
|
#ifdef HAVE_CHEATS
|
|
void command_event_init_cheats(
|
|
bool apply_cheats_after_load,
|
|
const char *path_cheat_db,
|
|
void *bsv_movie_data)
|
|
{
|
|
#ifdef HAVE_NETWORKING
|
|
bool allow_cheats = !netplay_driver_ctl(
|
|
RARCH_NETPLAY_CTL_IS_DATA_INITED, NULL);
|
|
#else
|
|
bool allow_cheats = true;
|
|
#endif
|
|
#ifdef HAVE_BSV_MOVIE
|
|
allow_cheats &= !(bsv_movie_data != NULL);
|
|
#endif
|
|
|
|
if (!allow_cheats)
|
|
return;
|
|
|
|
cheat_manager_alloc_if_empty();
|
|
cheat_manager_load_game_specific_cheats(path_cheat_db);
|
|
|
|
if (apply_cheats_after_load)
|
|
cheat_manager_apply_cheats(
|
|
config_get_ptr()->bools.notification_show_cheats_applied);
|
|
}
|
|
#endif
|
|
|
|
bool command_event_load_entry_state(settings_t *settings)
|
|
{
|
|
char entry_state_path[PATH_MAX_LENGTH] = "";
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
bool ret = false;
|
|
|
|
/* No early save-capability gate here: content_load_state() decides,
|
|
* and an existing entry-state file outranks stale metadata. */
|
|
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
return false;
|
|
#endif
|
|
#ifdef HAVE_NETWORKING
|
|
if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL))
|
|
return false;
|
|
#endif
|
|
|
|
if (!runloop_get_entry_state_path(
|
|
entry_state_path, sizeof(entry_state_path),
|
|
runloop_st->entry_state_slot))
|
|
return false;
|
|
|
|
if (!path_is_valid(entry_state_path))
|
|
{
|
|
if (!runloop_get_savestate_path(
|
|
entry_state_path, sizeof(entry_state_path),
|
|
runloop_st->entry_state_slot))
|
|
return false;
|
|
}
|
|
|
|
if (!path_is_valid(entry_state_path))
|
|
return false;
|
|
|
|
ret = content_load_state(entry_state_path, false, true);
|
|
|
|
RARCH_LOG("[State] %s \"%s\".\n",
|
|
msg_hash_to_str(MSG_FOUND_ENTRY_STATE_IN),
|
|
entry_state_path);
|
|
RARCH_LOG("[State] %s \"%s\" %s.\n",
|
|
msg_hash_to_str(MSG_LOADING_ENTRY_STATE_FROM),
|
|
entry_state_path,
|
|
ret ? "succeeded" : "failed");
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool command_event_load_auto_state(void)
|
|
{
|
|
size_t _len;
|
|
char savestate_name_auto[PATH_MAX_LENGTH];
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_savestate = runloop_st->name.savestate;
|
|
bool ret = false;
|
|
|
|
/* No early save-capability gate here: content_load_state() decides,
|
|
* and an existing .auto state file outranks stale metadata. */
|
|
|
|
#ifdef HAVE_CHEEVOS
|
|
if (rcheevos_hardcore_active())
|
|
return false;
|
|
#endif
|
|
#ifdef HAVE_NETWORKING
|
|
if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL))
|
|
return false;
|
|
#endif
|
|
|
|
_len = strlcpy(savestate_name_auto, name_savestate,
|
|
sizeof(savestate_name_auto));
|
|
strlcpy_lit(savestate_name_auto + _len, ".auto",
|
|
sizeof(savestate_name_auto) - _len);
|
|
|
|
if (!path_is_valid(savestate_name_auto))
|
|
return false;
|
|
|
|
ret = content_load_state(savestate_name_auto, false, true);
|
|
|
|
RARCH_LOG("[State] %s \"%s\".\n",
|
|
msg_hash_to_str(MSG_FOUND_AUTO_SAVESTATE_IN),
|
|
savestate_name_auto);
|
|
RARCH_LOG("[State] %s \"%s\" %s.\n",
|
|
msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM),
|
|
savestate_name_auto,
|
|
ret ? "succeeded" : "failed");
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Scans existing states to determine which one should be loaded
|
|
* and which one can be deleted, using savestate wraparound if
|
|
* enabled.
|
|
*
|
|
* @param settings The usual RetroArch settings ptr.
|
|
* @param last_index Return value for load slot.
|
|
* @param @s Return value for file name that should be removed.
|
|
*/
|
|
static void command_scan_states(
|
|
bool show_hidden_files,
|
|
unsigned savestate_max_keep,
|
|
int curr_state_slot,
|
|
unsigned *last_index, char *s)
|
|
{
|
|
/* Base name of 128 may be too short for some (<<1%) of the
|
|
tosec-based file names, but in practice truncating will not
|
|
lead to mismatch */
|
|
char state_base[128];
|
|
char state_dir[DIR_MAX_LENGTH];
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_savestate = runloop_st->name.savestate;
|
|
|
|
unsigned max_idx = 0;
|
|
unsigned loa_idx = 0;
|
|
unsigned gap_idx = UINT_MAX;
|
|
unsigned del_idx = UINT_MAX;
|
|
retro_bits_512_t slot_mapping_low = {0};
|
|
retro_bits_512_t slot_mapping_high = {0};
|
|
|
|
struct string_list *dir_list = NULL;
|
|
const char *savefile_root = NULL;
|
|
size_t savefile_root_length = 0;
|
|
|
|
size_t i, cnt = 0;
|
|
size_t cnt_in_range = 0;
|
|
|
|
fill_pathname_basedir(state_dir, name_savestate,
|
|
sizeof(state_dir));
|
|
|
|
if (!(dir_list = dir_list_new_special(state_dir,
|
|
DIR_LIST_PLAIN, NULL, show_hidden_files)))
|
|
return;
|
|
|
|
fill_pathname_base(state_base, name_savestate, sizeof(state_base));
|
|
|
|
for (i = 0; i < dir_list->size; i++)
|
|
{
|
|
unsigned idx;
|
|
size_t _len;
|
|
char elem_base[128];
|
|
const char *ext = NULL;
|
|
const char *end = NULL;
|
|
const char *dir_elem = dir_list->elems[i].data;
|
|
|
|
if (!dir_elem || !*dir_elem)
|
|
continue;
|
|
|
|
_len = strlen(dir_elem);
|
|
fill_pathname_base(elem_base, dir_elem, sizeof(elem_base));
|
|
|
|
/* Only consider files with a '.state' extension
|
|
* > i.e. Ignore '.state.auto', '.state.bak', etc. */
|
|
ext = path_get_extension(elem_base);
|
|
if ( (!ext || !*ext)
|
|
|| !string_starts_with_size(ext, "state", STRLEN_CONST("state")))
|
|
continue;
|
|
|
|
/* Check whether this file is associated with
|
|
* the current content */
|
|
if (!string_starts_with(elem_base, state_base))
|
|
continue;
|
|
|
|
/* This looks like a valid savestate */
|
|
/* Save filename root and length (once) */
|
|
if (savefile_root_length == 0)
|
|
{
|
|
savefile_root = dir_elem;
|
|
savefile_root_length = _len;
|
|
}
|
|
|
|
/* Decode the savestate index */
|
|
end = dir_elem + _len;
|
|
while ((end > dir_elem) && ISDIGIT((int)end[-1]))
|
|
{
|
|
end--;
|
|
if (savefile_root == dir_elem)
|
|
savefile_root_length--;
|
|
}
|
|
idx = string_to_unsigned(end);
|
|
|
|
/* Simple administration: max, total. */
|
|
if (idx > max_idx)
|
|
max_idx = idx;
|
|
cnt++;
|
|
if (idx <= savestate_max_keep)
|
|
cnt_in_range++;
|
|
|
|
/* Maintain a 2x512 bit map of occupied save states */
|
|
if (idx < 512)
|
|
BIT512_SET(slot_mapping_low,idx);
|
|
else if (idx < 1024)
|
|
BIT512_SET(slot_mapping_high, idx - 512);
|
|
}
|
|
|
|
/* Next loop on the bitmap, since the file system may have presented the files in any order above */
|
|
for (i = 0; i <= savestate_max_keep; i++)
|
|
{
|
|
/* Unoccupied save slots */
|
|
if ( (i < 512 && !BIT512_GET(slot_mapping_low, i))
|
|
|| (i > 511 && !BIT512_GET(slot_mapping_high, i-512)))
|
|
{
|
|
/* Gap index: lowest free slot in the wraparound range */
|
|
if (gap_idx == UINT_MAX)
|
|
gap_idx = (unsigned)i;
|
|
}
|
|
else /* Occupied save slots */
|
|
{
|
|
/* Del index: first occupied slot in the wraparound range,
|
|
after gap index */
|
|
if ( gap_idx < UINT_MAX && del_idx == UINT_MAX)
|
|
del_idx = (unsigned)i;
|
|
}
|
|
}
|
|
|
|
/* Special cases of wraparound */
|
|
|
|
/* No previous savestate - set to end, so that first save
|
|
goes to 0 */
|
|
if (cnt_in_range == 0)
|
|
{
|
|
if (cnt == 0)
|
|
loa_idx = savestate_max_keep;
|
|
/* Transient: nothing in current range, but something is present
|
|
* higher up -> load that */
|
|
else
|
|
loa_idx = max_idx;
|
|
gap_idx = savestate_max_keep;
|
|
del_idx = savestate_max_keep;
|
|
}
|
|
/* No gap was found - deduct from current index or default
|
|
and set (missing) gap index to be deleted */
|
|
else if (gap_idx == UINT_MAX)
|
|
{
|
|
/* Transient: no gap, and max is higher than currently
|
|
* allowed -> load that, but wrap around so that next
|
|
* time gap will be present */
|
|
if (max_idx > savestate_max_keep)
|
|
{
|
|
loa_idx = max_idx;
|
|
gap_idx = 1;
|
|
}
|
|
/* Current index is in range, so let's assume it is correct */
|
|
else if ( (unsigned)curr_state_slot < savestate_max_keep)
|
|
{
|
|
loa_idx = curr_state_slot;
|
|
gap_idx = curr_state_slot + 1;
|
|
}
|
|
else
|
|
{
|
|
loa_idx = savestate_max_keep;
|
|
gap_idx = 0;
|
|
}
|
|
del_idx = gap_idx;
|
|
}
|
|
/* Gap was found */
|
|
else
|
|
{
|
|
/* No candidate to delete */
|
|
/* Either gap is at the end of the range: wraparound.
|
|
or there is no better idea than the lowest index */
|
|
if (del_idx == UINT_MAX)
|
|
del_idx = 0;
|
|
/* Adjust load index */
|
|
if (gap_idx == 0)
|
|
loa_idx = savestate_max_keep;
|
|
else
|
|
loa_idx = gap_idx - 1;
|
|
}
|
|
|
|
RARCH_DBG("[State] Save state scanning finished, used slots (in range): "
|
|
"%d (%d), max:%d, load index %d, gap index %d, delete index %d.\n",
|
|
cnt, cnt_in_range, max_idx, loa_idx, gap_idx, del_idx);
|
|
|
|
if (last_index)
|
|
*last_index = loa_idx;
|
|
|
|
if ( s
|
|
&& cnt_in_range >= savestate_max_keep)
|
|
{
|
|
strlcpy(s, savefile_root, savefile_root_length + 1);
|
|
/* ".state0" is just ".state" instead, so don't print that. */
|
|
if (del_idx > 0)
|
|
snprintf(s + savefile_root_length, 5, "%d", del_idx);
|
|
}
|
|
|
|
dir_list_free(dir_list);
|
|
}
|
|
|
|
/**
|
|
* Determines next savestate slot in case of auto-increment,
|
|
* i.e. save state scanning was done already earlier.
|
|
* Logic moved here so that all save state wraparound code is
|
|
* in this file.
|
|
*
|
|
* @param settings The usual RetroArch settings ptr.
|
|
* @return \c The next savestate slot.
|
|
*/
|
|
int command_event_get_next_savestate_auto_index(settings_t *settings)
|
|
{
|
|
unsigned savestate_max_keep = settings->uints.savestate_max_keep;
|
|
int new_state_slot = settings->ints.state_slot + 1;
|
|
/* If previous save was above the wraparound range, or it overflows,
|
|
return to the start of the range. */
|
|
if ( (savestate_max_keep > 0)
|
|
&& (unsigned)new_state_slot > savestate_max_keep)
|
|
return 0;
|
|
return new_state_slot;
|
|
}
|
|
|
|
/**
|
|
* Determines most recent savestate slot in case of content load.
|
|
*
|
|
* @param settings The usual RetroArch settings ptr.
|
|
* @return \c The most recent savestate slot.
|
|
*/
|
|
void command_event_set_savestate_auto_index(settings_t *settings)
|
|
{
|
|
unsigned max_idx = 0;
|
|
bool savestate_auto_index = settings->bools.savestate_auto_index;
|
|
if (savestate_auto_index)
|
|
{
|
|
int prev_slot = settings->ints.state_slot;
|
|
command_scan_states(
|
|
settings->bools.show_hidden_files,
|
|
settings->uints.savestate_max_keep,
|
|
settings->ints.state_slot, &max_idx, NULL);
|
|
configuration_set_int(settings, settings->ints.state_slot, max_idx);
|
|
RARCH_LOG("[State] %s: #%d (slot reset %d -> %u from on-disk scan, "
|
|
"max_keep %u). If the previous slot was higher, earlier saves "
|
|
"may be missing on disk.\n",
|
|
msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT),
|
|
max_idx, prev_slot, max_idx,
|
|
settings->uints.savestate_max_keep);
|
|
}
|
|
else
|
|
/* Reset savestate index to 0 when loading content. */
|
|
configuration_set_int(settings, settings->ints.state_slot, 0);
|
|
}
|
|
|
|
/**
|
|
* Deletes the oldest save state and its thumbnail, if needed.
|
|
*
|
|
* @param settings The usual RetroArch settings ptr.
|
|
*/
|
|
static void command_event_set_savestate_garbage_collect(settings_t *settings)
|
|
{
|
|
size_t i;
|
|
char state_to_delete[PATH_MAX_LENGTH] = {0};
|
|
command_scan_states(
|
|
settings->bools.show_hidden_files,
|
|
settings->uints.savestate_max_keep,
|
|
settings->ints.state_slot, NULL, state_to_delete);
|
|
/* Only delete one save state per save action
|
|
* > Conservative behaviour, designed to minimise
|
|
* the risk of deleting multiple incorrect files
|
|
* in case of accident */
|
|
if (*state_to_delete)
|
|
{
|
|
filestream_delete(state_to_delete);
|
|
RARCH_DBG("[State] Garbage collect, deleting \"%s\".\n",state_to_delete);
|
|
/* Construct the save state thumbnail name
|
|
* and delete that one as well. */
|
|
i = strlen(state_to_delete);
|
|
strlcpy_lit(state_to_delete + i,".png",STRLEN_CONST(".png")+1);
|
|
filestream_delete(state_to_delete);
|
|
RARCH_DBG("[State] Garbage collect, deleting \"%s\".\n",state_to_delete);
|
|
}
|
|
}
|
|
|
|
void command_event_set_replay_auto_index(settings_t *settings)
|
|
{
|
|
size_t i;
|
|
char elem_base[128];
|
|
char state_base[128];
|
|
char state_dir[DIR_MAX_LENGTH];
|
|
|
|
struct string_list *dir_list = NULL;
|
|
unsigned max_idx = 0;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_replay = runloop_st->name.replay;
|
|
bool replay_auto_index = settings->bools.replay_auto_index;
|
|
bool show_hidden_files = settings->bools.show_hidden_files;
|
|
|
|
if (!replay_auto_index)
|
|
return;
|
|
/* Find the file in the same directory as runloop_st->name.replay
|
|
* with the largest numeral suffix.
|
|
*
|
|
* E.g. /foo/path/content.replay will try to find
|
|
* /foo/path/content.replay%d, where %d is the largest number available.
|
|
*/
|
|
fill_pathname_basedir(state_dir, name_replay,
|
|
sizeof(state_dir));
|
|
|
|
if (!(dir_list = dir_list_new_special(state_dir,
|
|
DIR_LIST_PLAIN, NULL, show_hidden_files)))
|
|
return;
|
|
|
|
fill_pathname_base(state_base, name_replay, sizeof(state_base));
|
|
|
|
for (i = 0; i < dir_list->size; i++)
|
|
{
|
|
unsigned idx;
|
|
const char *end = NULL;
|
|
const char *dir_elem = dir_list->elems[i].data;
|
|
size_t _len = fill_pathname_base(elem_base, dir_elem, sizeof(elem_base));
|
|
|
|
if (strstr(elem_base, state_base) != elem_base)
|
|
continue;
|
|
|
|
end = dir_elem + _len;
|
|
|
|
while ((end > dir_elem) && ISDIGIT((int)end[-1]))
|
|
end--;
|
|
|
|
idx = (unsigned)strtoul(end, NULL, 0);
|
|
if (idx > max_idx)
|
|
max_idx = idx;
|
|
}
|
|
|
|
dir_list_free(dir_list);
|
|
|
|
configuration_set_int(settings, settings->ints.replay_slot, max_idx);
|
|
|
|
if (max_idx)
|
|
RARCH_LOG("[Replay] %s: #%d\n",
|
|
msg_hash_to_str(MSG_FOUND_LAST_REPLAY_SLOT),
|
|
max_idx);
|
|
}
|
|
|
|
void command_event_set_replay_garbage_collect(
|
|
unsigned max_to_keep,
|
|
bool show_hidden_files
|
|
)
|
|
{
|
|
/* TODO: debugme */
|
|
size_t i, cnt = 0;
|
|
char tmp[DIR_MAX_LENGTH];
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
const char *name_replay = runloop_st->name.replay;
|
|
struct string_list *dir_list = NULL;
|
|
unsigned min_idx = UINT_MAX;
|
|
const char *oldest_save = NULL;
|
|
|
|
/* Similar to command_event_set_replay_auto_index(),
|
|
* this will find the lowest numbered replay */
|
|
fill_pathname_basedir(tmp, name_replay, sizeof(tmp));
|
|
|
|
if (!(dir_list = dir_list_new_special(tmp,
|
|
DIR_LIST_PLAIN, NULL, show_hidden_files)))
|
|
return;
|
|
|
|
fill_pathname_base(tmp, name_replay, sizeof(tmp));
|
|
|
|
for (i = 0; i < dir_list->size; i++)
|
|
{
|
|
unsigned idx;
|
|
size_t _len;
|
|
char elem_base[128];
|
|
const char *ext = NULL;
|
|
const char *end = NULL;
|
|
const char *dir_elem = dir_list->elems[i].data;
|
|
|
|
if (!dir_elem || !*dir_elem)
|
|
continue;
|
|
|
|
_len = fill_pathname_base(elem_base, dir_elem, sizeof(elem_base));
|
|
|
|
/* Only consider files with a '.replayXX' extension
|
|
* > i.e. Ignore '.replay.auto', '.replay.bak', etc. */
|
|
ext = path_get_extension(elem_base);
|
|
if ( (!ext || !*ext)
|
|
|| !string_starts_with_size(ext, "replay", STRLEN_CONST("REPLAY")))
|
|
continue;
|
|
|
|
/* Check whether this file is associated with
|
|
* the current content */
|
|
if (!string_starts_with(elem_base, tmp))
|
|
continue;
|
|
|
|
/* This looks like a valid save */
|
|
cnt++;
|
|
|
|
/* > Get index */
|
|
end = dir_elem + _len;
|
|
|
|
while ((end > dir_elem) && ISDIGIT((int)end[-1]))
|
|
end--;
|
|
|
|
idx = string_to_unsigned(end);
|
|
|
|
/* > Check if this is the lowest index so far */
|
|
if (idx < min_idx)
|
|
{
|
|
min_idx = idx;
|
|
oldest_save = dir_elem;
|
|
}
|
|
}
|
|
|
|
/* Only delete one save state per save action
|
|
* > Conservative behaviour, designed to minimise
|
|
* the risk of deleting multiple incorrect files
|
|
* in case of accident */
|
|
if (oldest_save && *oldest_save && (cnt > max_to_keep))
|
|
filestream_delete(oldest_save);
|
|
|
|
dir_list_free(dir_list);
|
|
}
|
|
|
|
#if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL)
|
|
bool command_set_shader(command_t *cmd, const char *arg)
|
|
{
|
|
enum rarch_shader_type type = video_shader_parse_type(arg);
|
|
settings_t *settings = config_get_ptr();
|
|
bool apply_new_shader = arg && *arg;
|
|
|
|
configuration_set_bool(settings, settings->bools.video_shader_enable, apply_new_shader);
|
|
if (apply_new_shader)
|
|
{
|
|
gfx_ctx_flags_t flags;
|
|
flags.flags = 0;
|
|
video_context_driver_get_flags(&flags);
|
|
|
|
if (!BIT32_GET(flags.flags, video_shader_type_to_flag(type)))
|
|
return false;
|
|
|
|
/* rebase on shader directory */
|
|
if (!path_is_absolute(arg))
|
|
{
|
|
char abs_arg[PATH_MAX_LENGTH];
|
|
const char *ref_path = settings->paths.directory_video_shader;
|
|
fill_pathname_join_special(abs_arg, ref_path, arg, sizeof(abs_arg));
|
|
return video_shader_apply_shader(settings, type, abs_arg, true);
|
|
}
|
|
}
|
|
|
|
return video_shader_apply_shader(settings, type, arg, true);
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_CONFIGFILE
|
|
bool command_event_save_core_config(
|
|
const char *dir_menu_config,
|
|
const char *rarch_path_config)
|
|
{
|
|
char msg[128];
|
|
char config_dir[DIR_MAX_LENGTH];
|
|
char config_path[PATH_MAX_LENGTH];
|
|
char config_name[NAME_MAX_LENGTH];
|
|
size_t _len = 0;
|
|
bool new_path_available = false;
|
|
bool overrides_active = false;
|
|
const char *core_path = NULL;
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
|
|
msg[0] = '\0';
|
|
|
|
if (dir_menu_config && *dir_menu_config)
|
|
_len = strlcpy(config_dir, dir_menu_config, sizeof(config_dir));
|
|
else if (rarch_path_config && *rarch_path_config) /* Fallback */
|
|
_len = fill_pathname_basedir(config_dir, rarch_path_config,
|
|
sizeof(config_dir));
|
|
|
|
if (_len == 0)
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_CONFIG_DIRECTORY_NOT_SET);
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
RARCH_ERR("[Config] %s\n", _msg);
|
|
return false;
|
|
}
|
|
|
|
core_path = path_get(RARCH_PATH_CORE);
|
|
|
|
/* Infer file name based on libretro core. */
|
|
if (path_is_valid(core_path))
|
|
{
|
|
unsigned i;
|
|
char tmp[PATH_MAX_LENGTH + 8];
|
|
RARCH_LOG("[Config] %s\n", msg_hash_to_str(MSG_USING_CORE_NAME_FOR_NEW_CONFIG));
|
|
|
|
fill_pathname(config_name, path_basename(core_path), "",
|
|
sizeof(config_name));
|
|
fill_pathname_join_special(config_path, config_dir, config_name,
|
|
sizeof(config_path));
|
|
|
|
/* In case of collision, find an alternative name. */
|
|
for (i = 0; i < 16; i++)
|
|
{
|
|
size_t __len = strlcpy(tmp, config_path, sizeof(tmp));
|
|
|
|
if (i)
|
|
__len += snprintf(tmp + __len, sizeof(tmp) - __len, "-%u", i);
|
|
strlcpy_lit(tmp + __len, ".cfg", sizeof(tmp) - __len);
|
|
|
|
if (!path_is_valid(tmp))
|
|
{
|
|
strlcpy(config_path, tmp, sizeof(config_path));
|
|
new_path_available = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!new_path_available)
|
|
{
|
|
/* Fallback to system time... */
|
|
RARCH_WARN("[Config] %s\n",
|
|
msg_hash_to_str(MSG_CANNOT_INFER_NEW_CONFIG_PATH));
|
|
fill_dated_filename(config_name, ".cfg", sizeof(config_name));
|
|
fill_pathname_join_special(config_path, config_dir, config_name,
|
|
sizeof(config_path));
|
|
}
|
|
|
|
if (runloop_st->flags & RUNLOOP_FLAG_OVERRIDES_ACTIVE)
|
|
{
|
|
/* Overrides block config file saving,
|
|
* make it appear as overrides weren't enabled
|
|
* for a manual save. */
|
|
runloop_st->flags &= ~RUNLOOP_FLAG_OVERRIDES_ACTIVE;
|
|
overrides_active = true;
|
|
}
|
|
|
|
#ifdef HAVE_CONFIGFILE
|
|
_len = command_event_save_config(config_path, msg, sizeof(msg));
|
|
#endif
|
|
|
|
if (_len > 0)
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
|
|
if (overrides_active)
|
|
runloop_st->flags |= RUNLOOP_FLAG_OVERRIDES_ACTIVE;
|
|
else
|
|
runloop_st->flags &= ~RUNLOOP_FLAG_OVERRIDES_ACTIVE;
|
|
|
|
return true;
|
|
}
|
|
|
|
void command_event_save_current_config(enum override_type type)
|
|
{
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
|
|
switch (type)
|
|
{
|
|
default:
|
|
case OVERRIDE_NONE:
|
|
{
|
|
size_t _len;
|
|
char msg[256];
|
|
enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO;
|
|
|
|
msg[0] = '\0';
|
|
|
|
if (path_is_empty(RARCH_PATH_CONFIG))
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy_lit(msg, "Config directory not set, cannot save configuration.", sizeof(msg));
|
|
}
|
|
else
|
|
{
|
|
if (runloop_st->flags & RUNLOOP_FLAG_OVERRIDES_ACTIVE)
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ACTIVE_NOT_SAVING), sizeof(msg));
|
|
}
|
|
else
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS;
|
|
_len = command_event_save_config(path_get(RARCH_PATH_CONFIG), msg, sizeof(msg));
|
|
}
|
|
}
|
|
|
|
/* command_event_save_config() does its own logging */
|
|
if (msg_cat == MESSAGE_QUEUE_CATEGORY_ERROR)
|
|
RARCH_ERR("[Override] %s\n", msg);
|
|
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, msg_cat);
|
|
}
|
|
break;
|
|
case OVERRIDE_GAME:
|
|
case OVERRIDE_CORE:
|
|
case OVERRIDE_CONTENT_DIR:
|
|
{
|
|
size_t _len;
|
|
char msg[256];
|
|
int8_t ret = config_save_overrides(type, &runloop_st->system, false, NULL);
|
|
enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO;
|
|
|
|
switch (ret)
|
|
{
|
|
case 1:
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS;
|
|
_len = strlcpy(msg,
|
|
msg_hash_to_str(MSG_OVERRIDES_SAVED_SUCCESSFULLY), sizeof(msg));
|
|
/* set overrides to active so the original config can be
|
|
restored after closing content */
|
|
runloop_st->flags |= RUNLOOP_FLAG_OVERRIDES_ACTIVE;
|
|
break;
|
|
case -1:
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_WARNING;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_NOT_SAVED), sizeof(msg));
|
|
break;
|
|
default:
|
|
case 0:
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_SAVING), sizeof(msg));
|
|
break;
|
|
}
|
|
|
|
switch (msg_cat)
|
|
{
|
|
case MESSAGE_QUEUE_CATEGORY_ERROR:
|
|
RARCH_ERR("[Override] %s\n", msg);
|
|
break;
|
|
case MESSAGE_QUEUE_CATEGORY_WARNING:
|
|
RARCH_WARN("[Override] %s\n", msg);
|
|
break;
|
|
case MESSAGE_QUEUE_CATEGORY_SUCCESS:
|
|
default:
|
|
RARCH_LOG("[Override] %s\n", msg);
|
|
break;
|
|
}
|
|
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, msg_cat);
|
|
|
|
#ifdef HAVE_MENU
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH
|
|
| MENU_ST_FLAG_PREVENT_POPULATE;
|
|
}
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void command_event_remove_current_config(enum override_type type)
|
|
{
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
|
|
switch (type)
|
|
{
|
|
default:
|
|
case OVERRIDE_NONE:
|
|
break;
|
|
case OVERRIDE_GAME:
|
|
case OVERRIDE_CORE:
|
|
case OVERRIDE_CONTENT_DIR:
|
|
{
|
|
size_t _len;
|
|
char msg[256];
|
|
enum message_queue_category msg_cat = MESSAGE_QUEUE_CATEGORY_INFO;
|
|
|
|
if (config_save_overrides(type, &runloop_st->system, true, NULL))
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_SUCCESS;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_REMOVED_SUCCESSFULLY), sizeof(msg));
|
|
}
|
|
else
|
|
{
|
|
msg_cat = MESSAGE_QUEUE_CATEGORY_ERROR;
|
|
_len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_REMOVING), sizeof(msg));
|
|
}
|
|
|
|
if (msg_cat == MESSAGE_QUEUE_CATEGORY_ERROR)
|
|
RARCH_ERR("[Override] %s\n", msg);
|
|
else
|
|
RARCH_LOG("[Override] %s\n", msg);
|
|
|
|
runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, msg_cat);
|
|
#ifdef HAVE_MENU
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH
|
|
| MENU_ST_FLAG_PREVENT_POPULATE;
|
|
}
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool command_event_main_state(unsigned cmd)
|
|
{
|
|
char msg[128] = "";
|
|
char state_path[PATH_MAX_LENGTH] = "";
|
|
size_t _len = 0;
|
|
settings_t *settings = config_get_ptr();
|
|
bool savestates_enabled = core_info_current_supports_savestate();
|
|
bool ret = false;
|
|
|
|
if (savestates_enabled)
|
|
{
|
|
size_t info_size;
|
|
runloop_get_current_savestate_path(state_path,
|
|
sizeof(state_path));
|
|
|
|
info_size = core_serialize_size();
|
|
savestates_enabled = (info_size > 0);
|
|
}
|
|
|
|
/* TODO: Load state should act in one of three ways:
|
|
- [X] Not during recording or playback: normally
|
|
- [-] During playback: If the state is part of this replay, go back to
|
|
that state and rewind the replay (not yet implemented); otherwise
|
|
halt playback and go to that state normally.
|
|
- [-] During recording: If the state is part of this replay, go back to
|
|
that state and rewind the replay, clobbering the stuff in between
|
|
then and now (not yet implemented); if the state is not part of
|
|
the replay, do nothing and log a warning.
|
|
*/
|
|
if (savestates_enabled)
|
|
{
|
|
switch (cmd)
|
|
{
|
|
case CMD_EVENT_SAVE_STATE:
|
|
case CMD_EVENT_SAVE_STATE_TO_RAM:
|
|
{
|
|
/* TODO: Saving state during recording should associate
|
|
* the state with the replay. */
|
|
video_driver_state_t *video_st =
|
|
video_state_get_ptr();
|
|
bool savestate_auto_index =
|
|
settings->bools.savestate_auto_index;
|
|
unsigned savestate_max_keep =
|
|
settings->uints.savestate_max_keep;
|
|
bool frame_time_counter_auto_reset =
|
|
settings->bools.frame_time_counter_auto_reset;
|
|
|
|
if (cmd == CMD_EVENT_SAVE_STATE)
|
|
{
|
|
bool queued = content_save_state(state_path, true);
|
|
RARCH_LOG("[State] save dispatch for slot %d, path "
|
|
"\"%s\": content_save_state queued=%s "
|
|
"(auto_index=%s, max_keep=%u). NOTE: actual disk "
|
|
"write is asynchronous; success is only known when "
|
|
"the save task completes.\n",
|
|
settings->ints.state_slot, state_path,
|
|
queued ? "yes" : "NO",
|
|
savestate_auto_index ? "on" : "off",
|
|
savestate_max_keep);
|
|
}
|
|
else
|
|
content_save_state_to_ram();
|
|
|
|
/* Clean up excess savestates if necessary */
|
|
if (savestate_auto_index && (savestate_max_keep > 0))
|
|
command_event_set_savestate_garbage_collect(settings);
|
|
|
|
if (frame_time_counter_auto_reset)
|
|
video_st->frame_time_count = 0;
|
|
|
|
ret = true;
|
|
}
|
|
break;
|
|
case CMD_EVENT_LOAD_STATE:
|
|
case CMD_EVENT_LOAD_STATE_FROM_RAM:
|
|
{
|
|
bool res = false;
|
|
if (cmd == CMD_EVENT_LOAD_STATE)
|
|
res = content_load_state(state_path, false, false);
|
|
else
|
|
res = content_load_state_from_ram();
|
|
|
|
if (res)
|
|
{
|
|
command_post_state_loaded();
|
|
ret = true;
|
|
}
|
|
}
|
|
break;
|
|
case CMD_EVENT_UNDO_LOAD_STATE:
|
|
{
|
|
/* TODO: To support this through re-recording would take some
|
|
* care around moving the replay recording forward to the time
|
|
* when the undo happened, which would need undo support for
|
|
* replays. For now, forbid it during recording and halt
|
|
* playback. */
|
|
#ifdef HAVE_BSV_MOVIE
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_RECORDING)
|
|
{
|
|
RARCH_ERR("[State] Can't undo load state during movie record.\n");
|
|
return false;
|
|
}
|
|
if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_PLAYBACK)
|
|
{
|
|
RARCH_LOG("[State] Undo load state during movie playback, halting playback.\n");
|
|
movie_stop(input_st);
|
|
}
|
|
#endif
|
|
_len = command_event_undo_load_state(msg, sizeof(msg));
|
|
ret = true;
|
|
break;
|
|
}
|
|
case CMD_EVENT_UNDO_SAVE_STATE:
|
|
_len = command_event_undo_save_state(msg, sizeof(msg));
|
|
ret = true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
_len = strlcpy(msg, msg_hash_to_str(
|
|
MSG_CORE_DOES_NOT_SUPPORT_SAVESTATES), sizeof(msg));
|
|
|
|
if (_len > 0)
|
|
{
|
|
runloop_msg_queue_push(msg, _len, 2, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
RARCH_LOG("[State] %s\n", msg);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool command_event_disk_control_append_image(
|
|
const char *path)
|
|
{
|
|
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
|
rarch_system_info_t *sys_info = runloop_st ? (rarch_system_info_t*)&runloop_st->system : NULL;
|
|
if ( !sys_info
|
|
|| !disk_control_append_image(&sys_info->disk_control, path))
|
|
return false;
|
|
|
|
#ifdef HAVE_THREADS
|
|
if (runloop_st->flags & RUNLOOP_FLAG_USE_SRAM)
|
|
autosave_deinit();
|
|
#endif
|
|
|
|
command_event(CMD_EVENT_AUTOSAVE_INIT, NULL);
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Read-side callback for the snapshot phase of command_event_reinit.
|
|
* Receives the cached frame's pixels (or NULL if HW-render / no
|
|
* cached frame), copies into the static reuse buffer, and reports
|
|
* dims back to the caller via userdata. Skips the work entirely
|
|
* if data is NULL -- caller checks the reported size to know
|
|
* whether a snapshot was actually taken. */
|
|
struct command_reinit_snapshot_ctx
|
|
{
|
|
void **buf_p; /* static cached_snapshot in the caller */
|
|
size_t *cap_p; /* static cached_snapshot_cap in the caller */
|
|
unsigned 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();
|
|
input_driver_state_t *input_st = input_state_get_ptr();
|
|
video_driver_state_t *video_st = video_state_get_ptr();
|
|
#ifdef HAVE_MENU
|
|
gfx_display_t *p_disp = disp_get_ptr();
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
bool video_fullscreen = settings->bools.video_fullscreen;
|
|
bool adaptive_vsync = settings->bools.video_adaptive_vsync;
|
|
unsigned swap_interval_config = settings->uints.video_swap_interval;
|
|
#endif
|
|
enum input_game_focus_cmd_type
|
|
game_focus_cmd = GAME_FOCUS_CMD_REAPPLY;
|
|
const input_device_driver_t
|
|
*joypad = input_st->primary_joypad;
|
|
#ifdef HAVE_MFI
|
|
const input_device_driver_t
|
|
*sec_joypad = input_st->secondary_joypad;
|
|
#else
|
|
const input_device_driver_t
|
|
*sec_joypad = NULL;
|
|
#endif
|
|
|
|
/* Snapshot the last cached core frame before tearing the video
|
|
* driver down. video_driver_free() invalidates the cache as
|
|
* part of the reinit cycle (the pointer was borrowed from the
|
|
* core's own framebuffer and isn't guaranteed to stay live
|
|
* across the driver swap), so without a snapshot the new
|
|
* driver would come up with no core image to replay. Restored
|
|
* + replayed below so the paused-core background remains
|
|
* visible when reinit is triggered from inside the menu (e.g.
|
|
* HDR mode toggle).
|
|
*
|
|
* The snapshot buffer is static and reused across reinits — we
|
|
* need it to outlive command_event_reinit because we publish
|
|
* the pointer back through video_driver_cached_frame_publish
|
|
* for the new driver to read via video_driver_cached_frame(),
|
|
* and the core's next real frame will replace the pointer at
|
|
* its leisure. Resizing in place on each call keeps it
|
|
* bounded at one buffer's worth.
|
|
*
|
|
* The actual copy happens inside the cached_frame_read
|
|
* callback (which is what this function was hand-rolling
|
|
* before the unified read API existed -- exactly the same
|
|
* pattern, lifted into one place). HW-render frames are
|
|
* skipped: the callback receives data == NULL for those, so
|
|
* we don't even allocate. */
|
|
static void *cached_snapshot = NULL;
|
|
static size_t cached_snapshot_cap = 0;
|
|
unsigned cached_snapshot_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.
|
|
*
|
|
* Nothing can be ungrabbed with the drivers down, but the grab
|
|
* flag is bookkeeping the win32 focus pump and the grab toggle
|
|
* read later, so leave it as the skipped reinit's game-focus
|
|
* reapply would have: released, unless exclusive fullscreen
|
|
* (which re-grabs on init), auto-grab or game focus keeps it.
|
|
* Everything below reuses this function's own locals. */
|
|
if (!video_st->data)
|
|
{
|
|
if ( !settings->bools.video_fullscreen
|
|
&& !(video_driver_get_disp_flags() & VIDEO_FLAG_FORCE_FULLSCREEN)
|
|
&& !settings->bools.input_auto_mouse_grab
|
|
&& !input_st->game_focus_state.enabled)
|
|
input_st->flags &= ~INP_FLAG_GRAB_MOUSE_STATE;
|
|
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);
|
|
|
|
/* 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. */
|
|
if (joypad && joypad->poll)
|
|
joypad->poll();
|
|
if (sec_joypad && sec_joypad->poll)
|
|
sec_joypad->poll();
|
|
if (input_st->current_driver && input_st->current_driver->poll)
|
|
input_st->current_driver->poll(input_st->current_data);
|
|
command_event(CMD_EVENT_GAME_FOCUS_TOGGLE, &game_focus_cmd);
|
|
|
|
#ifdef HAVE_MENU
|
|
p_disp->flags |= GFX_DISP_FLAG_FB_DIRTY;
|
|
if (video_fullscreen)
|
|
{
|
|
if ( video_st->poke
|
|
&& video_st->poke->show_mouse)
|
|
video_st->poke->show_mouse(video_st->data, false);
|
|
}
|
|
if ( (menu_st->flags & MENU_ST_FLAG_ALIVE)
|
|
&& video_st->current_video->set_nonblock_state)
|
|
video_st->current_video->set_nonblock_state(
|
|
video_st->data, false,
|
|
video_driver_test_all_flags(GFX_CTX_FLAGS_ADAPTIVE_VSYNC)
|
|
&& adaptive_vsync,
|
|
runloop_get_video_swap_interval(swap_interval_config));
|
|
#endif
|
|
}
|