/*  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 <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"

#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_reset_after_load_state =
       config_get_ptr()->bools.frame_time_counter_reset_after_load_state;
     if (frame_time_counter_reset_after_load_state)
        video_st->frame_time_count = 0;
   }
}

#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 \"%s\" failed.\n", arg);
      }
      else
         handle->state[map[index].id] = true;
   }
   else
      RARCH_WARN(msg_hash_to_str(MSG_UNRECOGNIZED_COMMAND), tok);
}

static void command_parse_msg(command_t *handle, char *buf)
{
   char     *save  = NULL;
   const char *tok = strtok_r(buf, "\n", &save);

   while (tok)
   {
      command_parse_sub_msg(handle, tok);
      tok = strtok_r(NULL, "\n", &save);
   }
}

#if defined(HAVE_NETWORK_CMD)
typedef struct
{
   /* Network socket FD */
   int net_fd;
   /* Source address for the command received */
   struct sockaddr_storage cmd_source;
   /* Size of the previous structure in use */
   socklen_t cmd_source_len;
} command_network_t;

static void network_command_reply(command_t *cmd,
   const char *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;
   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);
   }
}

command_t* command_network_new(uint16_t port)
{
   struct addrinfo     *res  = NULL;
   command_t            *cmd = (command_t*)calloc(1, sizeof(*cmd));
   command_network_t *netcmd = (command_network_t*)calloc(
                                   1, sizeof(command_network_t));
   int fd = socket_init((void**)&res, port, NULL,
         SOCKET_TYPE_DATAGRAM, AF_INET);

   RARCH_LOG("[NetCMD]: %s %hu.\n",
         msg_hash_to_str(MSG_BRINGING_UP_COMMAND_INTERFACE_ON_PORT),
         (unsigned short)port);

   if (fd < 0)
      goto error;

   netcmd->net_fd = fd;
   cmd->userptr   = netcmd;
   cmd->poll      = command_network_poll;
   cmd->replier   = network_command_reply;
   cmd->destroy   = network_command_free;

   if (!socket_nonblock(netcmd->net_fd))
      goto error;

   if (!socket_bind(netcmd->net_fd, (void*)res))
   {
      RARCH_ERR("[NetCMD]: %s.\n",
            msg_hash_to_str(MSG_FAILED_TO_BIND_SOCKET));
      goto error;
   }

   freeaddrinfo_retro(res);
   return cmd;

error:
   if (res)
      freeaddrinfo_retro(res);
   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)
{
   ptrdiff_t msg_len;
   char        *last_newline = NULL;
   command_stdin_t *stdincmd = (command_stdin_t*)handle->userptr;
   ssize_t               ret = read_stdin(
         stdincmd->stdin_buf + stdincmd->stdin_buf_ptr,
         CMD_BUF_SIZE - stdincmd->stdin_buf_ptr - 1);

   if (ret == 0)
      return;

   stdincmd->stdin_buf_ptr                      += ret;
   stdincmd->stdin_buf[stdincmd->stdin_buf_ptr]  = '\0';

   last_newline = strrchr(stdincmd->stdin_buf, '\n');

   if (!last_newline)
   {
      /* We're receiving bogus data in pipe
       * (no terminating newline), flush out the buffer. */
      if (stdincmd->stdin_buf_ptr + 1 >= CMD_BUF_SIZE)
      {
         stdincmd->stdin_buf_ptr = 0;
         stdincmd->stdin_buf[0]  = '\0';
      }

      return;
   }

   *last_newline++ = '\0';
   msg_len         = last_newline - stdincmd->stdin_buf;

   command_parse_msg(handle, stdincmd->stdin_buf);

   memmove(stdincmd->stdin_buf, last_newline,
         stdincmd->stdin_buf_ptr - msg_len);
   stdincmd->stdin_buf_ptr -= msg_len;
}

command_t* command_stdin_new(void)
{
   command_t *cmd;
   command_stdin_t *stdincmd;

#if !(defined(_WIN32) || defined(EMSCRIPTEN))
#ifdef HAVE_NETWORKING
   if (!socket_nonblock(STDIN_FILENO))
      return NULL;
#endif
#endif

   cmd          = (command_t*)calloc(1, sizeof(command_t));
   stdincmd     = (command_stdin_t*)calloc(1, sizeof(command_stdin_t));

   if (!cmd)
      return NULL;
   if (!stdincmd)
   {
      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)
      return;
   command_parse_msg(handle, emscriptencmd->command_buf);
}

command_t* command_emscripten_new(void)
{
   command_t *cmd;
   command_emscripten_t *emscriptencmd;

   cmd           = (command_t*)calloc(1, sizeof(command_t));
   emscriptencmd = (command_emscripten_t*)calloc(1, sizeof(command_emscripten_t));

   if (!cmd)
      return NULL;
   if (!emscriptencmd)
   {
      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 (string_is_equal(arg, "video_fullscreen"))
   {
      if (video_fullscreen)
         value = "true";
      else
         value = "false";
   }
   else if (string_is_equal(arg, "savefile_directory"))
      value = dir_get_ptr(RARCH_DIR_SAVEFILE);
   else if (string_is_equal(arg, "savestate_directory"))
      value = dir_get_ptr(RARCH_DIR_SAVESTATE);
   else if (string_is_equal(arg, "runtime_log_directory"))
      value = dir_runtime_log;
   else if (string_is_equal(arg, "log_dir"))
      value = log_dir;
   else if (string_is_equal(arg, "cache_directory"))
      value = directory_cache;
   else if (string_is_equal(arg, "system_directory"))
      value = directory_system;
   else if (string_is_equal(arg, "netplay_nickname"))
      value = path_username;
#ifdef HAVE_BSV_MOVIE
   else if (string_is_equal(arg, "active_replay"))
   {
      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(value_dynamic, "0 0 0", sizeof(value_dynamic));
   }
   #endif
   /* TODO: query any string */

   _len  = strlcpy(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)
{
   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);
      }
      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;
   strcpy(&addr.sun_path[1], "retroarch/cmd");

   if (bind(fd, (struct sockaddr*)&addr, addrsz) < 0 ||
       listen(fd, MAX_USER_CONNECTIONS) < 0)
   {
      socket_close(fd);
      return NULL;
   }

   if (!socket_nonblock(fd))
   {
      socket_close(fd);
      return NULL;
   }

   cmd             = (command_t*)calloc(1, sizeof(command_t));
   subcmd          = (command_uds_t*)calloc(1, sizeof(command_uds_t));
   subcmd->sfd     = fd;
   subcmd->last_fd = -1;
   for (i = 0; i < MAX_USER_CONNECTIONS; i++)
      subcmd->userfd[i] = -1;

   cmd->userptr = subcmd;
   cmd->poll    = command_uds_poll;
   cmd->replier = uds_command_reply;
   cmd->destroy = uds_command_free;

   return cmd;
}
#endif


/* Routines used to invoke retroarch command ... */

#ifdef HAVE_NETWORK_CMD
static bool command_verify(const char *cmd)
{
   unsigned i;

   if (command_get_arg(cmd, NULL, NULL))
      return true;

   RARCH_ERR("[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("\t\t%s\n", map[i].str);

   for (i = 0; i < ARRAY_SIZE(action_map); i++)
      RARCH_ERR("\t\t%s %s\n", action_map[i].str, action_map[i].arg_desc);

   return false;
}

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 *command        = NULL;
   char *save           = NULL;
   const char *cmd      = NULL;

   if (!network_init())
      return false;

   if (!(command = strdup(cmd_)))
      return false;

   cmd                  = strtok_r(command, ";", &save);
   if (cmd)
   {
      uint16_t port     = DEFAULT_NETWORK_CMD_PORT;
      const char *port_ = NULL;
      const char *host  = strtok_r(NULL, ";", &save);
      if (host)
         port_          = strtok_r(NULL, ";", &save);
      else
      {
#ifdef _WIN32
         host = "127.0.0.1";
#else
         host = "localhost";
#endif
      }

      if (port_)
         port = strtoul(port_, NULL, 0);

      RARCH_LOG("[NetCMD]: %s: \"%s\" to %s:%hu\n",
            msg_hash_to_str(MSG_SENDING_COMMAND),
            cmd, host, (unsigned short)port);

      if (command_verify(cmd) && udp_send_packet(host, port, cmd))
      {
         free(command);
         return true;
      }
   }

   free(command);
   return false;
}
#endif

bool command_show_osd_msg(command_t *cmd, const char* arg)
{
    runloop_msg_queue_push(arg, 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(reply, "LOAD_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)
   {
      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_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();
         task_queue_wait(NULL, 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
}


#if defined(HAVE_CHEEVOS)
bool command_read_ram(command_t *cmd, const char *arg)
{
   unsigned i;
   char *reply                  = NULL;
   const uint8_t  *data         = NULL;
   char *reply_at               = NULL;
   unsigned int nbytes          = 0;
   unsigned int alloc_size      = 0;
   unsigned int addr            = -1;
   size_t _len                  = 0;

   if (sscanf(arg, "%x %u", &addr, &nbytes) != 2)
      return true;
   /* We allocate more than needed, saving 20 bytes is not really relevant */
   alloc_size              = 40 + nbytes * 3;
   reply                   = (char*)malloc(alloc_size);
   reply[0]                = '\0';
   reply_at                = reply + snprintf(
         reply, alloc_size - 1, "READ_CORE_RAM" " %x", addr);

   if ((data = rcheevos_patch_address(addr)))
   {
      for (i = 0; i < nbytes; i++)
         snprintf(reply_at + 3 * i, 4, " %.2X", data[i]);
      reply_at[3 * nbytes] = '\n';
      _len                 = reply_at + 3 * nbytes + 1 - reply;
   }
   else
   {
      strlcpy(reply_at, " -1\n", sizeof(reply) - strlen(reply));
      _len                  = reply_at + STRLEN_CONST(" -1\n") - reply;
   }
   cmd->replier(cmd, reply, _len);
   free(reply);
   return true;
}

bool command_write_ram(command_t *cmd, const char *arg)
{
   unsigned int addr    = (unsigned int)strtoul(arg, (char**)&arg, 16);
   uint8_t *data        = (uint8_t *)rcheevos_patch_address(addr);

   if (!data)
      return false;

   if (rcheevos_hardcore_active())
   {
      RARCH_LOG("[Command]: Achievements hardcore mode disabled by WRITE_CORE_RAM.\n");
      rcheevos_pause_hardcore();
   }

   while (*arg)
   {
      *data = strtoul(arg, (char**)&arg, 16);
      data++;
   }
   return true;
}
#endif

bool command_version(command_t *cmd, const char* arg)
{
   char reply[256];
   size_t  _len  = strlcpy(reply, PACKAGE_VERSION, sizeof(reply));
   reply[  _len] = '\n';
   reply[++_len] = '\0';
   cmd->replier(cmd, reply, _len);
   return true;
}

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(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(s, " -1 no descriptor for address\n", len);
      else if (!desc->core.ptr)
         strlcpy(s, " -1 no data for descriptor\n", len);
      else if (for_write && (desc->core.flags & RETRO_MEMDESC_CONST))
         strlcpy(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                  = 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();

      core_info_get_current_core(&core_info);

      _len     = strlcpy(reply, "GET_STATUS ", sizeof(reply));
      if (runloop_st->flags & RUNLOOP_FLAG_PAUSED)
         _len += strlcpy(reply + _len, "PAUSED", sizeof(reply) - _len);
      else
         _len += strlcpy(reply + _len, "PLAYING", sizeof(reply) - _len);
      _len    += strlcpy(reply + _len, " ", sizeof(reply) - _len);
      if (core_info)
         _len += strlcpy(reply + _len, core_info->system_id,
               sizeof(reply) - _len);
      else
         _len += strlcpy(reply + _len, runloop_st->system.info.library_name,
               sizeof(reply) - _len);
      _len    += strlcpy(reply + _len, ",", sizeof(reply) - _len);
      _len    += strlcpy(reply + _len,
            path_basename(path_get(RARCH_PATH_BASENAME)), sizeof(reply) - _len);
      _len    += snprintf(reply + _len, sizeof(reply) - _len,
            ",crc32=%lx\n", (unsigned long)content_get_crc());
   }
   else
       _len = strlcpy(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;

   if (sscanf(arg, "%x %u", &address, &nbytes) != 2)
      return false;

   /* Ensure large enough to return all requested bytes or an error message */
   alloc_size = 64 + nbytes * 3;
   reply      = (char*)malloc(alloc_size);
   reply_at   = reply + snprintf(reply, alloc_size - 1, "READ_CORE_MEMORY %x", address);

   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));
   msg[_len  ]      = ':';
   msg[++_len]      = ' ';
   msg[++_len]      = '\0';
   _len            += snprintf(msg + _len, sizeof(msg) - _len, "%.1f",
         new_volume);
   msg[_len  ]      = ' ';
   msg[++_len]      = 'd';
   msg[++_len]      = 'B';
   msg[++_len]      = '\0';

#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));
   msg[_len  ]      = ':';
   msg[++_len]      = ' ';
   msg[++_len]      = '\0';
   _len            += snprintf(msg + _len, sizeof(msg) - _len, "%.1f",
         new_volume);
   msg[_len  ]      = ' ';
   msg[++_len]      = 'd';
   msg[++_len]      = 'B';
   msg[++_len]      = '\0';
   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++)
   {
      unsigned i;
      retro_ctx_controller_info_t pad;
      unsigned device                                 = RETRO_DEVICE_NONE;
      const struct retro_controller_description *desc = NULL;

      /* Check whether current core port is mapped
       * to an input device
       * > If is not, leave 'device' set to
       *   'RETRO_DEVICE_NONE'
       * > For example: if input ports 0 and 1 are
       *   mapped to core port 0, core port 1 will
       *   be unmapped and should be disabled */
      for (i = 0; i < num_active_users; i++)
      {
         if (i >= MAX_USERS)
            break;

         if (port == settings->uints.input_remap_ports[i])
         {
            device = input_config_get_device(port);
            break;
         }
      }

      desc = libretro_find_controller_description(
            &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 = !string_is_empty(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 IOS
      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 (!string_is_empty(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())
      return strlcpy(s,
         msg_hash_to_str(MSG_NO_SAVE_STATE_HAS_BEEN_OVERWRITTEN_YET), 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())
      return strlcpy(s,
         msg_hash_to_str(MSG_NO_STATE_HAS_BEEN_LOADED_YET),
         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];
   if (runloop_st->entry_state_slot > -1)
      return 0;
   if (!core_info_current_supports_savestate())
      return 0;
   if (string_is_empty(path_basename(path_get(RARCH_PATH_BASENAME))))
      return 0;
   _len = strlcpy(savestate_name_auto, name_savestate,
         sizeof(savestate_name_auto));
   _len += strlcpy(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] = "";
   int entry_path_stats;
   runloop_state_t *runloop_st     = runloop_state_get_ptr();
   bool ret                        = false;

   if (!core_info_current_supports_savestate())
      return false;

#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;
   }

   entry_path_stats = path_stat(entry_state_path);

   if ((entry_path_stats & RETRO_VFS_STAT_IS_VALID) == 0
         || (entry_path_stats & RETRO_VFS_STAT_IS_DIRECTORY) != 0)
      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"
         );

   if (ret)
      configuration_set_int(settings, settings->ints.state_slot, runloop_st->entry_state_slot);

   return ret;
}

void 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;

   if (!core_info_current_supports_savestate())
      return;

#ifdef HAVE_CHEEVOS
   if (rcheevos_hardcore_active())
      return;
#endif
#ifdef HAVE_NETWORKING
   if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL))
      return;
#endif

   _len = strlcpy(savestate_name_auto, name_savestate,
         sizeof(savestate_name_auto));
   strlcpy(savestate_name_auto + _len, ".auto",
         sizeof(savestate_name_auto) - _len);

   if (!path_is_valid(savestate_name_auto))
      return;

   RARCH_LOG("[State]: %s \"%s\".\n",
         msg_hash_to_str(MSG_FOUND_AUTO_SAVESTATE_IN),
         savestate_name_auto);

   if ((content_load_state(savestate_name_auto, false, true)))
      RARCH_LOG("[State]: %s \"%s\" %s.\n",
            msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM),
            savestate_name_auto, "succeeded");
   else
      RARCH_LOG("[State]: %s \"%s\" %s.\n",
            msg_hash_to_str(MSG_AUTOLOADING_SAVESTATE_FROM),
            savestate_name_auto, "failed");
}

/**
 * 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 (string_is_empty(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 (    string_is_empty(ext)
          || !string_starts_with_size(ext, "state", STRLEN_CONST("state")))
         continue;

      /* Check whether this file is associated with
       * the current content */
      if (!string_starts_with(elem_base, state_base))
         continue;

      /* This looks like a valid 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)
   {
      /* Reset savestate index to 0 when loading content. */
      configuration_set_int(settings, settings->ints.state_slot, 0);
      return;
   }
   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.\n",
         msg_hash_to_str(MSG_FOUND_LAST_STATE_SLOT),
         max_idx);
}

/**
 * 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 (!string_is_empty(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(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 (string_is_empty(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 (    string_is_empty(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 (!string_is_empty(oldest_save) && (cnt > max_to_keep))
      filestream_delete(oldest_save);

   dir_list_free(dir_list);
}

#if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL)
bool command_set_shader(command_t *cmd, const char *arg)
{
   enum  rarch_shader_type type = video_shader_parse_type(arg);
   settings_t  *settings        = config_get_ptr();

   if (!string_is_empty(arg))
   {
      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 (!string_is_empty(dir_menu_config))
      _len = strlcpy(config_dir, dir_menu_config, sizeof(config_dir));
   else if (!string_is_empty(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_INFO);
      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(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];

            msg[0] = '\0';

            if (path_is_empty(RARCH_PATH_CONFIG))
            {
               _len = strlcpy(msg, "Config directory not set, cannot save configuration.", sizeof(msg));
               runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
                     MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
            }
            else
            {
               if (runloop_st->flags & RUNLOOP_FLAG_OVERRIDES_ACTIVE)
                  _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ACTIVE_NOT_SAVING), sizeof(msg));
               else
                  _len = command_event_save_config(path_get(RARCH_PATH_CONFIG), msg, sizeof(msg));
               runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
                     MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
            }
         }
         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);

            switch (ret)
            {
               case 1:
                  _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:
                  _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_NOT_SAVED), sizeof(msg));
                  break;
               default:
               case 0:
                  _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_SAVING), sizeof(msg));
                  break;
            }

            RARCH_LOG("[Overrides]: %s\n", msg);
            runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
                  MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);

#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];
            if (config_save_overrides(type, &runloop_st->system, true, NULL))
               _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_REMOVED_SUCCESSFULLY), sizeof(msg));
            else
               _len = strlcpy(msg, msg_hash_to_str(MSG_OVERRIDES_ERROR_REMOVING), sizeof(msg));

            RARCH_LOG("[Overrides]: %s\n", msg);
            runloop_msg_queue_push(msg, _len, 1, 180, true, NULL,
                  MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
#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_reset_after_save_state =
                     settings->bools.frame_time_counter_reset_after_save_state;

               if (cmd == CMD_EVENT_SAVE_STATE)
                  content_save_state(state_path, true);
               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_reset_after_save_state)
                  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

   /* TODO/FIXME: Need to figure out what to do with subsystems case. */
   if (path_is_empty(RARCH_PATH_SUBSYSTEM))
   {
      /* Update paths for our new image.
       * If we actually use append_image, we assume that we
       * started out in a single disk case, and that this way
       * of doing it makes the most sense. */
      path_set(RARCH_PATH_NAMES, path);
      runloop_path_fill_names();
   }

   command_event(CMD_EVENT_AUTOSAVE_INIT, NULL);

   return true;
}

void command_event_reinit(const int flags)
{
   settings_t *settings           = config_get_ptr();
   input_driver_state_t *input_st = input_state_get_ptr();
   video_driver_state_t *video_st = video_state_get_ptr();
#ifdef HAVE_MENU
   gfx_display_t *p_disp          = disp_get_ptr();
   struct menu_state *menu_st     = menu_state_get_ptr();
   bool video_fullscreen          = settings->bools.video_fullscreen;
   bool adaptive_vsync            = settings->bools.video_adaptive_vsync;
   unsigned swap_interval_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

   video_driver_reinit(flags);
   /* Poll input to avoid possibly stale data to corrupt things. */
   if (joypad && joypad->poll)
      joypad->poll();
   if (sec_joypad && sec_joypad->poll)
      sec_joypad->poll();
   if (input_st->current_driver && input_st->current_driver->poll)
      input_st->current_driver->poll(input_st->current_data);
   command_event(CMD_EVENT_GAME_FOCUS_TOGGLE, &game_focus_cmd);

#ifdef HAVE_MENU
   p_disp->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
}
