From 0955667f0d648539478405d00c150eaac1726fa7 Mon Sep 17 00:00:00 2001 From: Gregor Richards Date: Thu, 16 Feb 2017 19:59:40 -0500 Subject: [PATCH] Adding ranetplayer netplay regression testing tool --- tools/ranetplayer/Makefile | 20 ++ tools/ranetplayer/README | 3 + tools/ranetplayer/ranetplayer.c | 417 ++++++++++++++++++++++++++++++++ 3 files changed, 440 insertions(+) create mode 100644 tools/ranetplayer/Makefile create mode 100644 tools/ranetplayer/README create mode 100644 tools/ranetplayer/ranetplayer.c diff --git a/tools/ranetplayer/Makefile b/tools/ranetplayer/Makefile new file mode 100644 index 0000000000..68ef90dd2e --- /dev/null +++ b/tools/ranetplayer/Makefile @@ -0,0 +1,20 @@ +CC=gcc +CFLAGS=-O3 -g +INCLUDES=-I../../libretro-common/include + +OBJS=ranetplayer.o compat_getopt.o net_compat.o net_socket.o + +ranetplayer: $(OBJS) + $(CC) $(CFLAGS) $(INCLUDES) $(OBJS) -o $@ + +%.o: %.c + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +compat_%.o: ../..//libretro-common/compat/compat_%.c + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +net_%.o: ../../libretro-common/net/net_%.c + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +clean: + rm -f $(OBJS) ranetplayer diff --git a/tools/ranetplayer/README b/tools/ranetplayer/README new file mode 100644 index 0000000000..405ad2f386 --- /dev/null +++ b/tools/ranetplayer/README @@ -0,0 +1,3 @@ +ranetplayer is a small tool for recording and playing back netplay sessions. It +is primarily intended as a regression testing tool, but can be used as a +general-purpose input movie recorder and player. diff --git a/tools/ranetplayer/ranetplayer.c b/tools/ranetplayer/ranetplayer.c new file mode 100644 index 0000000000..a9660a91e2 --- /dev/null +++ b/tools/ranetplayer/ranetplayer.c @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2017 Gregor Richards + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include "compat/getopt.h" +#include "net/net_socket.h" + +/* Only for #defines */ +#include "../../network/netplay/netplay_private.h" + +/* Macros to handle basic I/O */ +#define ERROR() do { \ + fprintf(stderr, "Netplay disconnected.\n"); \ + exit(0); \ +} while(0) + +#define EXPAND() do { \ + while (cmd_size > payload_size) \ + { \ + payload_size *= 2; \ + payload = realloc(payload, payload_size); \ + if (!payload) \ + { \ + perror("realloc"); \ + exit(1); \ + } \ + } \ +} while (0) + +#define RECV() do { \ + if (!socket_receive_all_blocking(sock, &cmd, sizeof(uint32_t)) || \ + !socket_receive_all_blocking(sock, &cmd_size, sizeof(uint32_t))) \ + ERROR(); \ + cmd = ntohl(cmd); \ + cmd_size = ntohl(cmd_size); \ + EXPAND(); \ + if (!socket_receive_all_blocking(sock, payload, cmd_size)) \ + ERROR(); \ +} while(0) + +#define SEND() do { \ + uint32_t adj_cmd[2]; \ + adj_cmd[0] = htonl(cmd); \ + adj_cmd[1] = htonl(cmd_size); \ + if (!socket_send_all_blocking(sock, adj_cmd, sizeof(adj_cmd), true) || \ + !socket_send_all_blocking(sock, payload, cmd_size, true)) \ + ERROR(); \ +} while(0) + +/* Our fds */ +static int sock, ranp_in, ranp_out; + +/* Space for netplay packets */ +static uint32_t cmd, cmd_size, *payload; +static size_t payload_size; + +/* The frame number when we connected, to offset frames being played or + * recorded */ +static uint32_t frame_offset = 0; + +/* Usage statement */ +void usage() +{ + fprintf(stderr, + "Use: ranetplayer [options] [ranp file]\n" + "Options:\n" + " -H|--host
: Netplay host. Defaults to localhost.\n" + " -P|--port : Netplay port. Defaults to 55435.\n" + " -p|--play : Play back a recording over netplay.\n" + " -r|--record : Record netplay to a file.\n" + " -a|--ahead : Number of frames by which to play ahead of the\n" + " server. Tests rewind if negative, catch-up if\n" + " positive.\n" + "\n"); +} + +/* Offset the frame in a network packet to or from network time */ +uint32_t frame_offset_cmd(bool ntoh) +{ + uint32_t frame = 0; + + switch (cmd) + { + case NETPLAY_CMD_INPUT: + case NETPLAY_CMD_NOINPUT: + case NETPLAY_CMD_MODE: + case NETPLAY_CMD_CRC: + case NETPLAY_CMD_LOAD_SAVESTATE: + case NETPLAY_CMD_RESET: + case NETPLAY_CMD_FLIP_PLAYERS: + frame = ntohl(payload[0]); + if (ntoh) + frame -= frame_offset; + else + frame += frame_offset; + payload[0] = htonl(frame); + break; + } + + return frame; +} + +/* Send a bit of our input */ +bool send_input(uint32_t cur_frame) +{ + while (1) + { + uint32_t rd_frame = 0; + + if (read(ranp_in, &cmd, sizeof(uint32_t)) != sizeof(uint32_t) || + read(ranp_in, &cmd_size, sizeof(uint32_t)) != sizeof(uint32_t)) + return false; + + cmd = ntohl(cmd); + cmd_size = ntohl(cmd_size); + EXPAND(); + + if (read(ranp_in, payload, cmd_size) != cmd_size) + return false; + + /* Adjust the frame for commands we know */ + rd_frame = frame_offset_cmd(false); + if (rd_frame) + rd_frame -= frame_offset; + + /* And send it */ + SEND(); + + if (rd_frame > cur_frame) + break; + } + + return true; +} + +int main(int argc, char **argv) +{ + struct addrinfo *addr; + uint32_t rd_frame = 0; + int ahead = 0; + const char *host = "localhost", + *ranp_in_file_name = NULL, + *ranp_out_file_name = NULL; + int port = RARCH_DEFAULT_PORT; + bool playing = false, playing_started = false, + recording = false, recording_started = false; + const char *optstring = NULL; + + const struct option opt[] = { + {"host", 1, NULL, 'H'}, + {"port", 1, NULL, 'P'}, + {"play", 1, NULL, 'p'}, + {"record", 1, NULL, 'r'}, + {"ahead", 1, NULL, 'a'} + }; + + while (1) + { + int c; + + c = getopt_long(argc, argv, "H:P:p:r:a:", opt, NULL); + if (c == -1) + break; + + switch (c) + { + case 'H': + host = optarg; + break; + + case 'P': + port = atoi(optarg); + break; + + case 'p': + playing = true; + ranp_in_file_name = optarg; + break; + + case 'r': + recording = true; + ranp_out_file_name = optarg; + break; + + case 'a': + ahead = atoi(optarg); + break; + + default: + usage(); + return 1; + } + } + + if (!playing && optind < argc) + { + playing = true; + ranp_in_file_name = argv[optind++]; + } + if (!playing && !recording) + { + usage(); + return 1; + } + + /* Allocate space for the protocol */ + payload_size = 4096; + payload = malloc(payload_size); + if (!payload) + { + perror("malloc"); + return 1; + } + + /* Open the input file, if applicable */ + if (playing) + { + ranp_in = open(ranp_in_file_name, O_RDONLY); + if (ranp_in == -1) + { + perror(ranp_in_file_name); + return 1; + } + } + + /* Open the output file, if applicable */ + if (recording) + { + ranp_out = open(ranp_out_file_name, O_WRONLY|O_CREAT|O_EXCL, 0666); + if (ranp_out == -1) + { + perror(ranp_out_file_name); + return 1; + } + } + + /* Connect to the netplay server */ + if ((sock = socket_init((void **) &addr, port, host, SOCKET_PROTOCOL_TCP)) < 0) + { + perror("socket"); + return 1; + } + + if (socket_connect(sock, addr, false) < 0) + { + perror("connect"); + return 1; + } + + /* Expect the header */ + if (!socket_receive_all_blocking(sock, payload, 4*sizeof(uint32_t))) + { + fprintf(stderr, "Failed to receive connection header.\n"); + return 1; + } + + /* If it needs a password, too bad! */ + if (payload[3]) + { + fprintf(stderr, "Password required but unsupported.\n"); + return 1; + } + + /* Echo the connection header back */ + socket_send_all_blocking(sock, payload, 4*sizeof(uint32_t), true); + + /* Send a nickname */ + cmd = NETPLAY_CMD_NICK; + cmd_size = 32; + strcpy((char *) payload, "RANetplayer"); + SEND(); + + /* Receive (and ignore) the nickname */ + RECV(); + + /* Receive INFO */ + RECV(); + if (cmd != NETPLAY_CMD_INFO) + { + fprintf(stderr, "Failed to receive INFO."); + return 1; + } + + /* Echo the INFO */ + SEND(); + + /* Receive SYNC */ + RECV(); + + /* If we're recording and NOT playing, we start immediately in spectator + * mode */ + if (recording && !playing) + { + recording_started = true; + frame_offset = ntohl(payload[0]); + } + + /* If we're playing, request to enter PLAY mode */ + if (playing) + { + cmd = NETPLAY_CMD_PLAY; + cmd_size = 0; + SEND(); + } + + /* Now handle netplay commands */ + while (1) + { + RECV(); + + frame_offset_cmd(true); + + /* Record this command */ + if (recording && recording_started) + { + uint32_t tmp_cmd[2]; + tmp_cmd[0] = htonl(cmd); + tmp_cmd[1] = htonl(cmd_size); + + /* Write out this command */ + if (write(ranp_out, tmp_cmd, sizeof(tmp_cmd)) != sizeof(tmp_cmd) || + write(ranp_out, payload, cmd_size) != cmd_size) + { + perror(ranp_out_file_name); + close(ranp_out); + recording_started = recording = false; + if (!playing) + socket_close(sock); + } + } + + /* Now handle it for sync and playback */ + switch (cmd) + { + case NETPLAY_CMD_MODE: + if (playing && !playing_started) + { + uint32_t player; + + if (cmd_size < 2*sizeof(uint32_t)) break; + + /* See if this is us joining */ + player = ntohl(payload[1]); + if ((player & NETPLAY_CMD_MODE_BIT_PLAYING) && + (player & NETPLAY_CMD_MODE_BIT_YOU)) + { + /* This is where we start! */ + playing_started = true; + frame_offset_cmd(false); + frame_offset = ntohl(payload[0]); + + if (recording) + recording_started = true; + + /* Send our current input */ + send_input(0); + } + } + break; + + case NETPLAY_CMD_INPUT: + case NETPLAY_CMD_NOINPUT: + { + uint32_t frame; + + if (!playing || !playing_started) break; + if (cmd_size < sizeof(uint32_t)) break; + + frame = ntohl(payload[0]); + + /* Only sync based on server time */ + if (cmd == NETPLAY_CMD_INPUT && + (cmd_size < 2*sizeof(uint32_t) || + !(ntohl(payload[1]) & NETPLAY_CMD_INPUT_BIT_SERVER))) + { + break; + } + + if (frame > rd_frame) + { + rd_frame = frame; + if (ahead >= 0 || frame >= (uint32_t) -ahead) + { + if (!send_input(frame + ahead)) + { + if (!recording) + socket_close(sock); + playing = playing_started = false; + } + } + } + + break; + } + } + } + + return 0; +}