RetroArch/tools/ranetplayer/ranetplayer.c

426 lines
10 KiB
C

/*
* 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 <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#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)
#define WRITE() do { \
uint32_t adj_cmd[2]; \
adj_cmd[0] = htonl(cmd); \
adj_cmd[1] = htonl(cmd_size); \
if (write(ranp_out, adj_cmd, sizeof(adj_cmd)) != sizeof(adj_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); \
} \
} 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 <address>: Netplay host. Defaults to localhost.\n"
" -P|--port <port>: Netplay port. Defaults to 55435.\n"
" -p|--play <file>: Play back a recording over netplay.\n"
" -r|--record <file>: Record netplay to a file.\n"
" -a|--ahead <frames>: 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:
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)
{
for (;;)
{
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;
/* INFO is just saved for verification */
if (cmd == NETPLAY_CMD_INFO)
continue;
/* 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'}
};
for (;;)
{
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, 6*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, 6*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;
}
/* Save the INFO */
if (recording)
WRITE();
/* 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 = sizeof(uint32_t);
payload[0] = htonl(1);
SEND();
}
/* Now handle netplay commands */
for (;;)
{
RECV();
frame_offset_cmd(true);
/* Record this command */
if (recording && recording_started)
WRITE();
/* 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]) != 0)))
{
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;
}