Input test function for Remote RetroPad (#16357)

Add input test capability for Remote Retropad core:
- read an .ratst file (JSON format) with test steps
- display instruction (via libretro SET_MESSAGE)
- highlight input to be activated
- display summary when test is finished
This commit is contained in:
zoltanvb 2024-03-16 07:59:56 +01:00 committed by GitHub
parent bca3cc0165
commit 99aadc8651
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 463 additions and 12 deletions

View File

@ -0,0 +1,130 @@
[
{
"expected_button": 8,
"message": "Press A"
},
{
"expected_button": 0,
"message": "Press B"
},
{
"expected_button": 9,
"message": "Press X"
},
{
"expected_button": 1,
"message": "Press Y"
},
{
"expected_button": 2,
"message": "Press Select"
},
{
"expected_button": 3,
"message": "Press Start"
},
{
"expected_button": 4,
"message": "Press D-Pad Up"
},
{
"expected_button": 5,
"message": "Press D-Pad Down"
},
{
"expected_button": 6,
"message": "Press D-Pad Left"
},
{
"expected_button": 7,
"message": "Press D-Pad Right"
},
{
"expected_button": 10,
"message": "Press L1"
},
{
"expected_button": 11,
"message": "Press R1"
},
{
"expected_button": 12,
"message": "Press L2"
},
{
"expected_button": 13,
"message": "Press R2"
},
{
"expected_button": 14,
"message": "Press L3"
},
{
"expected_button": 15,
"message": "Press R3"
},
{
"expected_button": 25,
"message": "Move left analog stick up slightly"
},
{
"expected_button": 24,
"message": "Move left analog stick up fully"
},
{
"expected_button": 26,
"message": "Move left analog stick down slightly"
},
{
"expected_button": 27,
"message": "Move left analog stick down fully"
},
{
"expected_button": 17,
"message": "Move left analog stick left slightly"
},
{
"expected_button": 16,
"message": "Move left analog stick left fully"
},
{
"expected_button": 18,
"message": "Move left analog stick right slightly"
},
{
"expected_button": 19,
"message": "Move left analog stick right fully"
},
{
"expected_button": 29,
"message": "Move right analog stick up slightly"
},
{
"expected_button": 28,
"message": "Move right analog stick up fully"
},
{
"expected_button": 30,
"message": "Move right analog stick down slightly"
},
{
"expected_button": 31,
"message": "Move right analog stick down fully"
},
{
"expected_button": 21,
"message": "Move right analog stick left slightly"
},
{
"expected_button": 20,
"message": "Move right analog stick left fully"
},
{
"expected_button": 22,
"message": "Move right analog stick right slightly"
},
{
"expected_button": 23,
"message": "Move right analog stick right fully"
}
]

View File

@ -1,14 +1,14 @@
/*
* To-do:
* - Analog support
* - Some sort of connection control, it only sends packets
* but there is no acknoledgement of a connection o keepalives
* - Send player name
* - Render something on-screen maybe a gui to configure IP and port
instead of the ridiculously long strings we're using now
* - Allow changing IP address and port in runtime
* - Support other platforms
* - Input recording / Combos
* - Enable test input loading from menu
* - Visualization of keyboard and aux inputs (gyro, accelero, light)
*/
#include <stdio.h>
@ -26,6 +26,11 @@
#include <libretro.h>
#include <file/file_path.h>
#include <string/stdstring.h>
#include <streams/file_stream.h>
#include <formats/rjson.h>
#ifndef SOCKET_ERROR
#define SOCKET_ERROR -1
#endif
@ -49,6 +54,10 @@
id \
)
#define MAX_TEST_STEPS 200
#define INITIAL_FRAMES 60*5
#define ONE_TEST_STEP_FRAMES 60*5
struct descriptor {
int device;
int port_min;
@ -85,8 +94,6 @@ static retro_input_state_t NETRETROPAD_CORE_PREFIX(input_state_cb);
static uint16_t *frame_buf;
static unsigned input_state_validated = 0;
static struct descriptor joypad = {
.device = RETRO_DEVICE_JOYPAD,
.port_min = 0,
@ -112,10 +119,230 @@ static struct descriptor *descriptors[] = {
&analog
};
static uint16_t combo_def[] =
{
1 << RETRO_DEVICE_ID_JOYPAD_UP | 1 << RETRO_DEVICE_ID_JOYPAD_LEFT, /* D-pad diagonals */
1 << RETRO_DEVICE_ID_JOYPAD_UP | 1 << RETRO_DEVICE_ID_JOYPAD_RIGHT,
1 << RETRO_DEVICE_ID_JOYPAD_DOWN | 1 << RETRO_DEVICE_ID_JOYPAD_LEFT,
1 << RETRO_DEVICE_ID_JOYPAD_DOWN | 1 << RETRO_DEVICE_ID_JOYPAD_RIGHT,
1 << RETRO_DEVICE_ID_JOYPAD_UP | 1 << RETRO_DEVICE_ID_JOYPAD_DOWN, /* D-pad opposites */
1 << RETRO_DEVICE_ID_JOYPAD_LEFT | 1 << RETRO_DEVICE_ID_JOYPAD_RIGHT,
1 << RETRO_DEVICE_ID_JOYPAD_L3 | 1 << RETRO_DEVICE_ID_JOYPAD_R3, /* Combo values for menu / exit */
1 << RETRO_DEVICE_ID_JOYPAD_DOWN | 1 << RETRO_DEVICE_ID_JOYPAD_Y | 1 << RETRO_DEVICE_ID_JOYPAD_L | 1 << RETRO_DEVICE_ID_JOYPAD_R,
1 << RETRO_DEVICE_ID_JOYPAD_START | 1 << RETRO_DEVICE_ID_JOYPAD_SELECT | 1 << RETRO_DEVICE_ID_JOYPAD_L | 1 << RETRO_DEVICE_ID_JOYPAD_R,
1 << RETRO_DEVICE_ID_JOYPAD_START | 1 << RETRO_DEVICE_ID_JOYPAD_SELECT,
1 << RETRO_DEVICE_ID_JOYPAD_L3 | 1 << RETRO_DEVICE_ID_JOYPAD_R,
1 << RETRO_DEVICE_ID_JOYPAD_L | 1 << RETRO_DEVICE_ID_JOYPAD_R,
1 << RETRO_DEVICE_ID_JOYPAD_DOWN | 1 << RETRO_DEVICE_ID_JOYPAD_SELECT,
1 << RETRO_DEVICE_ID_JOYPAD_L2 | 1 << RETRO_DEVICE_ID_JOYPAD_R2
};
typedef struct
{
unsigned expected_button;
char message[PATH_MAX_LENGTH];
bool detected;
} 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 expected_button;
char *message;
} ITifJSONContext;
static bool ITifJSONObjectEndHandler(void* context)
{
ITifJSONContext *pCtx = (ITifJSONContext*)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 */
input_test_steps[current_test_step].expected_button = pCtx->expected_button;
if (!string_is_empty(pCtx->message))
strlcpy(
input_test_steps[current_test_step].message, pCtx->message,
sizeof(input_test_steps[current_test_step].message));
else
input_test_steps[current_test_step].message[0] = '\0';
current_test_step++;
last_test_step = current_test_step;
return true;
}
static bool ITifJSONObjectMemberHandler(void* context, const char *pValue, size_t length)
{
ITifJSONContext *pCtx = (ITifJSONContext*)context;
/* something went wrong */
if (pCtx->current_entry_str_val)
return false;
if (length)
{
if (string_is_equal(pValue, "expected_button"))
pCtx->current_entry_uint_val = &pCtx->expected_button;
else if (string_is_equal(pValue, "message"))
pCtx->current_entry_str_val = &pCtx->message;
/* ignore unknown members */
}
return true;
}
static bool ITifJSONNumberHandler(void* context, const char *pValue, size_t length)
{
ITifJSONContext *pCtx = (ITifJSONContext*)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 ITifJSONStringHandler(void* context, const char *pValue, size_t length)
{
ITifJSONContext *pCtx = (ITifJSONContext*)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;
ITifJSONContext context = {0};
RFILE *file = NULL;
rjson_t* parser;
/* Sanity check */
if ( string_is_empty(file_path)
|| !path_is_valid(file_path)
)
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)
{
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_ERROR,
"[Remote RetroPad]: Failed to open test input file: \"%s\".\n",
file_path);
return false;
}
/* Initialise JSON parser */
if (!(parser = rjson_open_rfile(file)))
{
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_ERROR,"[Remote RetroPad]: 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,
ITifJSONObjectMemberHandler,
ITifJSONStringHandler,
ITifJSONNumberHandler,
NULL, ITifJSONObjectEndHandler, NULL, NULL, /* object/array handlers */
NULL, NULL) /* unused boolean/null handlers */
!= RJSON_DONE)
{
if (rjson_get_source_context_len(parser))
{
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_ERROR,
"[Remote RetroPad]: 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));
}
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_WARN,
"[Remote RetroPad]: Error parsing test input file: %s\n",
file_path);
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_ERROR,
"[Remote RetroPad]: 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.message)
free(context.message);
/* Close log file */
filestream_close(file);
if (last_test_step >= MAX_TEST_STEPS)
{
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_WARN,"[Remote RetroPad]: 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++)
{
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_DEBUG,
"[Remote RetroPad]: test step %02d read from file: button %x, message %s\n",
current_test_step,
input_test_steps[current_test_step].expected_button,
input_test_steps[current_test_step].message);
}
current_test_step = 0;
return success;
}
/********************************/
/* Test input file handling end */
/********************************/
void NETRETROPAD_CORE_PREFIX(retro_init)(void)
{
unsigned i;
dump_state_blocked = false;
frame_buf = (uint16_t*)calloc(320 * 240, sizeof(uint16_t));
if (frame_buf)
@ -149,6 +376,7 @@ void NETRETROPAD_CORE_PREFIX(retro_init)(void)
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_INFO, "Initialising sockets...\n");
network_init();
}
void NETRETROPAD_CORE_PREFIX(retro_deinit)(void)
@ -185,8 +413,8 @@ void NETRETROPAD_CORE_PREFIX(retro_get_system_info)(
memset(info, 0, sizeof(*info));
info->library_name = "RetroPad Remote";
info->library_version = "0.01";
info->need_fullpath = false;
info->valid_extensions = ""; /* Nothing. */
info->need_fullpath = true;
info->valid_extensions = "ratst"; /* Special test input file. */
}
void NETRETROPAD_CORE_PREFIX(retro_get_system_av_info)(
@ -341,8 +569,10 @@ void NETRETROPAD_CORE_PREFIX(retro_run)(void)
int i;
unsigned rle;
uint32_t input_state = 0;
uint32_t expected_input = 0;
uint16_t *pixel = frame_buf + 49 * 320 + 32;
current_frame++;
/* Update input states and send them if needed */
retropad_update_input();
@ -367,7 +597,7 @@ void NETRETROPAD_CORE_PREFIX(retro_run)(void)
input_state |= 1 << (16 + i*8 + 3);
else if ((int16_t)analog.value[offset] > 3276)
input_state |= 1 << (16 + i*8 + 2);
offset = DESC_OFFSET(&analog, 0, RETRO_DEVICE_INDEX_ANALOG_RIGHT, i);
if ( (int16_t)analog.value[offset] < -32766/2)
input_state |= 1 << (16 + i*8 + 4);
@ -377,9 +607,80 @@ void NETRETROPAD_CORE_PREFIX(retro_run)(void)
input_state |= 1 << (16 + i*8 + 7);
else if ((int16_t)analog.value[offset] > 3276)
input_state |= 1 << (16 + i*8 + 6);
}
/* Input test section start. */
/* Check for predefined combo inputs. */
for (unsigned j = 0; j < sizeof(combo_def)/sizeof(combo_def[0]); j++)
{
if ((input_state & combo_def[j]) == combo_def[j])
combo_state_validated |= 1 << j;
}
/* Print a log for A+B combination, but only once while those are pressed */
if (input_state == ((1 << RETRO_DEVICE_ID_JOYPAD_A | 1 << RETRO_DEVICE_ID_JOYPAD_B) & 0x0000ffff))
{
if (!dump_state_blocked)
{
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_INFO,"[Remote RetroPad]: Validated state: %08x combo: %08x\n",input_state_validated, combo_state_validated);
dump_state_blocked = true;
}
}
else if (dump_state_blocked)
dump_state_blocked = false;
/* Handle test step proceeding and feedback to user */
if (current_test_step < last_test_step && current_frame > INITIAL_FRAMES)
{
if (current_frame > INITIAL_FRAMES + next_teststep_frame)
{
struct retro_message message;
if (current_frame > INITIAL_FRAMES + 1)
current_test_step++;
if (current_test_step < last_test_step)
{
message.msg = input_test_steps[current_test_step].message;
message.frames = ONE_TEST_STEP_FRAMES;
NETRETROPAD_CORE_PREFIX(environ_cb)(RETRO_ENVIRONMENT_SET_MESSAGE, &message);
next_teststep_frame = current_frame + ONE_TEST_STEP_FRAMES - INITIAL_FRAMES;
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_INFO,
"[Remote RetroPad]: Proceeding to test step %d at frame %d, next: %d\n",
current_test_step,current_frame,next_teststep_frame+INITIAL_FRAMES);
}
else
{
char buf[1024];
unsigned pass_count = 0;
for(unsigned i=0; i<last_test_step;i++)
if (input_test_steps[i].detected)
pass_count++;
message.msg = buf;
snprintf(buf,sizeof(buf),"Test sequence finished, result: %d/%d inputs detected",pass_count,last_test_step);
message.frames = ONE_TEST_STEP_FRAMES * 3;
NETRETROPAD_CORE_PREFIX(environ_cb)(RETRO_ENVIRONMENT_SET_MESSAGE, &message);
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_INFO,
"[Remote RetroPad]: Test sequence finished at frame %d, result: %d/%d inputs detected\n",
current_frame, pass_count, last_test_step);
}
}
if (current_test_step < last_test_step)
{
expected_input = 1 << input_test_steps[current_test_step].expected_button;
if(input_state & expected_input)
{
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_INFO,
"[Remote RetroPad]: Test step %d successful at frame %d\n",
current_test_step, current_frame);
input_test_steps[current_test_step].detected = true;
next_teststep_frame = current_frame - INITIAL_FRAMES;
}
}
}
/* Input test section end. */
for (rle = 0; rle < sizeof(retropad_buttons); )
{
unsigned runs;
@ -393,16 +694,22 @@ void NETRETROPAD_CORE_PREFIX(retro_run)(void)
{
unsigned count;
uint16_t color;
/* Red for active inputs */
if (input_state & button)
{
color = 0xA000;
input_state_validated |= button;
}
else
}
else
{
if (input_state_validated & button )
/* Light blue for expected input */
if (expected_input & button)
color = 0x7fff;
/* Light green for already validated input */
else if (input_state_validated & button )
color = 0xbff7;
/* White as default */
else
color = 0xffff;
}
@ -450,6 +757,20 @@ bool NETRETROPAD_CORE_PREFIX(retro_load_game)(const struct retro_game_info *info
NETRETROPAD_CORE_PREFIX(log_cb)(RETRO_LOG_INFO, "Server IP Address: %s\n" , server);
/* If a .ratst file is given (only possible via command line),
* initialize test sequence. */
if (info)
input_test_file_read(info->path);
if (last_test_step > MAX_TEST_STEPS)
current_test_step = last_test_step;
else
{
struct retro_message message;
message.msg = "Initiating test sequence...";
message.frames = INITIAL_FRAMES;
NETRETROPAD_CORE_PREFIX(environ_cb)(RETRO_ENVIRONMENT_SET_MESSAGE, &message);
}
return true;
}