/* RetroArch - A frontend for libretro. * Copyright (C) 2011-2017 - Daniel De Matteis * Copyright (C) 2016-2019 - Brad Parker * * 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 . */ /* Possible improvement list: * Add vid/pid to autoconf profile step, if autoconf matching needs testing * Multiple device autoconf profiles, with different analog/digital setup, analog buttons, extra buttons, unconfigured buttons... * Unimplemented functions (get_button, rumble) */ #include #include #include #include #include #include #include #include #include "../../config.def.h" #include "../../verbosity.h" #include "../input_driver.h" #include "../../tasks/tasks_internal.h" #include "../../gfx/video_driver.h" #define MAX_TEST_STEPS 200 #define NUM_BUTTONS 32 #ifndef MAX_AXIS #define MAX_AXIS 10 #endif #define JOYPAD_TEST_COMMAND_ADD_CONTROLLER 1 #define JOYPAD_TEST_COMMAND_BUTTON_PRESS_FIRST 16 #define JOYPAD_TEST_COMMAND_BUTTON_PRESS_LAST 31 #define JOYPAD_TEST_COMMAND_BUTTON_RELEASE_FIRST 32 #define JOYPAD_TEST_COMMAND_BUTTON_RELEASE_LAST 47 #define JOYPAD_TEST_COMMAND_BUTTON_AXIS_FIRST 1000 #define JOYPAD_TEST_COMMAND_BUTTON_AXIS_LAST JOYPAD_TEST_COMMAND_BUTTON_AXIS_FIRST+MAX_AXIS*DEFAULT_MAX_PADS typedef struct { char* name; uint32_t button_state; int32_t axis_state[MAX_AXIS]; } test_joypad_data; static test_joypad_data test_joypads[MAX_USERS]; typedef struct { unsigned frame; unsigned action; unsigned param_num; char param_str[255]; bool handled; } input_test_step_t; static input_test_step_t input_test_steps[MAX_TEST_STEPS]; static unsigned current_frame = 0; static unsigned next_teststep_frame = 0; static unsigned current_test_step = 0; static unsigned last_test_step = MAX_TEST_STEPS + 1; static uint32_t input_state_validated = 0; static uint32_t combo_state_validated = 0; static bool dump_state_blocked = false; /************************************/ /* JSON Helpers for test input file */ /************************************/ typedef struct { unsigned *current_entry_uint_val; char **current_entry_str_val; unsigned frame; unsigned action; unsigned param_num; char *param_str; } JTifJSONContext; static bool JTifJSONObjectEndHandler(void* context) { JTifJSONContext *pCtx = (JTifJSONContext*)context; /* Too long input is handled elsewhere, it should not lead to parse error */ if (current_test_step >= MAX_TEST_STEPS) return true; /* Copy values read from JSON file + fill defaults */ if (pCtx->frame == 0xffff) input_test_steps[current_test_step].frame = input_test_steps[current_test_step-1].frame + 60; else input_test_steps[current_test_step].frame = pCtx->frame; input_test_steps[current_test_step].action = pCtx->action; input_test_steps[current_test_step].param_num = pCtx->param_num; input_test_steps[current_test_step].handled = false; if (!string_is_empty(pCtx->param_str)) strlcpy( input_test_steps[current_test_step].param_str, pCtx->param_str, sizeof(input_test_steps[current_test_step].param_str)); else input_test_steps[current_test_step].param_str[0] = '\0'; current_test_step++; last_test_step = current_test_step; pCtx->frame = 0xffff; return true; } static bool JTifJSONObjectMemberHandler(void* context, const char *pValue, size_t length) { JTifJSONContext *pCtx = (JTifJSONContext*)context; /* something went wrong */ if (pCtx->current_entry_str_val) return false; if (length) { if (string_is_equal(pValue, "frame")) pCtx->current_entry_uint_val = &pCtx->frame; else if (string_is_equal(pValue, "action")) pCtx->current_entry_uint_val = &pCtx->action; else if (string_is_equal(pValue, "param_num")) pCtx->current_entry_uint_val = &pCtx->param_num; else if (string_is_equal(pValue, "param_str")) pCtx->current_entry_str_val = &pCtx->param_str; /* ignore unknown members */ } return true; } static bool JTifJSONNumberHandler(void* context, const char *pValue, size_t length) { JTifJSONContext *pCtx = (JTifJSONContext*)context; if (pCtx->current_entry_uint_val && length && !string_is_empty(pValue)) *pCtx->current_entry_uint_val = string_to_unsigned(pValue); /* ignore unknown members */ pCtx->current_entry_uint_val = NULL; return true; } static bool JTifJSONStringHandler(void* context, const char *pValue, size_t length) { JTifJSONContext *pCtx = (JTifJSONContext*)context; if (pCtx->current_entry_str_val && length && !string_is_empty(pValue)) { if (*pCtx->current_entry_str_val) free(*pCtx->current_entry_str_val); *pCtx->current_entry_str_val = strdup(pValue); } /* ignore unknown members */ pCtx->current_entry_str_val = NULL; return true; } /* Parses test input file referenced by file_path. * Does nothing if test input file does not exist. */ static bool input_test_file_read(const char* file_path) { bool success = false; JTifJSONContext context = {0}; RFILE *file = NULL; rjson_t* parser; /* Sanity check */ if ( string_is_empty(file_path) || !path_is_valid(file_path) ) { RARCH_DBG("[Test input driver]: No test input file supplied.\n"); return false; } /* Attempt to open test input file */ file = filestream_open( file_path, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); if (!file) { RARCH_ERR("[Test input driver]: Failed to open test input file: \"%s\".\n", file_path); return false; } /* Initialise JSON parser */ if (!(parser = rjson_open_rfile(file))) { RARCH_ERR("[Test input driver]: Failed to create JSON parser.\n"); goto end; } /* Configure parser */ rjson_set_options(parser, RJSON_OPTION_ALLOW_UTF8BOM); /* Read file */ if (rjson_parse(parser, &context, JTifJSONObjectMemberHandler, JTifJSONStringHandler, JTifJSONNumberHandler, NULL, JTifJSONObjectEndHandler, NULL, NULL, /* object/array handlers */ NULL, NULL) /* unused boolean/null handlers */ != RJSON_DONE) { if (rjson_get_source_context_len(parser)) { RARCH_ERR( "[Test input driver]: Error parsing chunk of test input file: %s\n---snip---\n%.*s\n---snip---\n", file_path, rjson_get_source_context_len(parser), rjson_get_source_context_buf(parser)); } RARCH_WARN( "[Test input driver]: Error parsing test input file: %s\n", file_path); RARCH_ERR( "[Test input driver]: Error: Invalid JSON at line %d, column %d - %s.\n", (int)rjson_get_source_line(parser), (int)rjson_get_source_column(parser), (*rjson_get_error(parser) ? rjson_get_error(parser) : "format error")); } /* Free parser */ rjson_free(parser); success = true; end: /* Clean up leftover strings */ if (context.param_str) free(context.param_str); /* Close log file */ filestream_close(file); if (last_test_step >= MAX_TEST_STEPS) { RARCH_WARN("[Test input driver]: too long test input json, maximum size: %d\n",MAX_TEST_STEPS); } for (current_test_step = 0; current_test_step < last_test_step; current_test_step++) { RARCH_DBG( "[Test input driver]: test step %02d read from file: frame %d, action %x, num %x, str %s\n", current_test_step, input_test_steps[current_test_step].frame, input_test_steps[current_test_step].action, input_test_steps[current_test_step].param_num, input_test_steps[current_test_step].param_str); } current_test_step = 0; return success; } /********************************/ /* Test input file handling end */ /********************************/ static const char *test_joypad_name(unsigned pad) { if (pad >= MAX_USERS || string_is_empty(test_joypads[pad].name)) return NULL; return test_joypads[pad].name; } static void test_joypad_autodetect_add(unsigned autoconf_pad) { input_autoconfigure_connect( test_joypad_name(autoconf_pad), NULL, "test", autoconf_pad, 0, 0 ); } static void *test_joypad_init(void *data) { settings_t *settings = config_get_ptr(); unsigned i; input_test_file_read(settings->paths.test_input_file_joypad); if (last_test_step > MAX_TEST_STEPS) last_test_step = 0; for(i=0; i 0) continue; if (input_test_steps[i].action == JOYPAD_TEST_COMMAND_ADD_CONTROLLER) { test_joypads[input_test_steps[i].param_num].name = input_test_steps[i].param_str; test_joypad_autodetect_add(input_test_steps[i].param_num); input_test_steps[i].handled = true; } } return (void*)-1; } static int32_t test_joypad_button(unsigned port_num, uint16_t joykey) { int16_t ret = 0; if (port_num >= DEFAULT_MAX_PADS) return 0; if (joykey < NUM_BUTTONS) return (BIT32_GET(test_joypads[port_num].button_state, joykey)); return 0; } static int16_t test_joypad_axis(unsigned port_num, uint32_t joyaxis) { /*RARCH_DBG("test_joypad_axis %d / %u\n",port_num, joyaxis);*/ if (port_num >= DEFAULT_MAX_PADS) return 0; if (AXIS_NEG_GET(joyaxis) < MAX_AXIS) { /* Kernel returns values in range [-0x7fff, 0x7fff]. */ int16_t val = test_joypads[port_num].axis_state[AXIS_NEG_GET(joyaxis)]; if (val < 0) return val; } else if (AXIS_POS_GET(joyaxis) < MAX_AXIS) { int16_t val = test_joypads[port_num].axis_state[AXIS_POS_GET(joyaxis)]; if (val > 0) return val; } return 0; } static int16_t test_joypad_state( rarch_joypad_info_t *joypad_info, const struct retro_keybind *binds, unsigned port) { unsigned i; int16_t ret = 0; uint16_t port_idx = joypad_info->joy_idx; if (port_idx < DEFAULT_MAX_PADS) { for (i = 0; i < RARCH_FIRST_CUSTOM_BIND; i++) { /* Auto-binds are per joypad, not per user. */ const uint16_t joykey = (binds[i].joykey != NO_BTN) ? binds[i].joykey : joypad_info->auto_binds[i].joykey; /* Test input driver uses same button layout internally as RA, so no conversion is needed */ if (joykey != NO_BTN && (test_joypads[port_idx].button_state & (1 << i))) { ret |= ( 1 << i); } } } return ret; } static void test_joypad_poll(void) { video_driver_state_t *video_st = video_state_get_ptr(); uint64_t curr_frame = video_st->frame_count; unsigned i; for (i=0; i input_test_steps[i].frame) { if (input_test_steps[i].action == JOYPAD_TEST_COMMAND_ADD_CONTROLLER) { test_joypads[input_test_steps[i].param_num].name = input_test_steps[i].param_str; test_joypad_autodetect_add(input_test_steps[i].param_num); input_test_steps[i].handled = true; } else if( input_test_steps[i].action >= JOYPAD_TEST_COMMAND_BUTTON_PRESS_FIRST && input_test_steps[i].action <= JOYPAD_TEST_COMMAND_BUTTON_PRESS_LAST) { unsigned targetpad = input_test_steps[i].action - JOYPAD_TEST_COMMAND_BUTTON_PRESS_FIRST; test_joypads[targetpad].button_state |= input_test_steps[i].param_num; input_test_steps[i].handled = true; RARCH_DBG( "[Test input driver]: Pressing device %d buttons %x, new state %x.\n", targetpad,input_test_steps[i].param_num,test_joypads[targetpad].button_state); } else if( input_test_steps[i].action >= JOYPAD_TEST_COMMAND_BUTTON_RELEASE_FIRST && input_test_steps[i].action <= JOYPAD_TEST_COMMAND_BUTTON_RELEASE_LAST) { unsigned targetpad = input_test_steps[i].action - JOYPAD_TEST_COMMAND_BUTTON_RELEASE_FIRST; test_joypads[targetpad].button_state &= ~input_test_steps[i].param_num; input_test_steps[i].handled = true; RARCH_DBG( "[Test input driver]: Releasing device %d buttons %x, new state %x.\n", targetpad,input_test_steps[i].param_num,test_joypads[targetpad].button_state); } else if( input_test_steps[i].action >= JOYPAD_TEST_COMMAND_BUTTON_AXIS_FIRST && input_test_steps[i].action <= JOYPAD_TEST_COMMAND_BUTTON_AXIS_LAST) { unsigned targetpad = (input_test_steps[i].action - JOYPAD_TEST_COMMAND_BUTTON_AXIS_FIRST) / MAX_AXIS; unsigned targetaxis = input_test_steps[i].action - JOYPAD_TEST_COMMAND_BUTTON_AXIS_FIRST - (targetpad*MAX_AXIS); if (targetpad < DEFAULT_MAX_PADS && targetaxis < MAX_AXIS) test_joypads[targetpad].axis_state[targetaxis] = (int16_t) input_test_steps[i].param_num; else RARCH_WARN( "[Test input driver]: Decoded axis outside target range: action %d pad %d axis %d.\n", input_test_steps[i].action, targetpad, targetaxis); input_test_steps[i].handled = true; RARCH_DBG( "[Test input driver]: Setting axis device %d axis %d value %d.\n", targetpad, targetaxis, (int16_t)input_test_steps[i].param_num); } else { input_test_steps[i].handled = true; RARCH_WARN( "[Test input driver]: Unrecognized action %d in step %d, skipping\n", input_test_steps[i].action,i); } } } } static bool test_joypad_query_pad(unsigned pad) { return (pad < MAX_USERS); } static void test_joypad_destroy(void) { } input_device_driver_t test_joypad = { test_joypad_init, test_joypad_query_pad, test_joypad_destroy, test_joypad_button, test_joypad_state, NULL, /* get_buttons */ test_joypad_axis, test_joypad_poll, NULL, /* rumble */ NULL, /* rumble_gain */ test_joypad_name, "test", };