Spectator mode.

This commit is contained in:
Themaister 2012-01-11 19:22:18 +01:00
parent 36a2c96cb7
commit 4beec3870f
7 changed files with 428 additions and 63 deletions

View File

@ -168,6 +168,15 @@ Set FRAMES to 0 to have perfect sync. 0 frames is only suitable for LAN. Default
\fB--port PORT\fR
Network port used for netplay. This defaults to 55435. This option affects both TCP and UDP.
.TP
\fB--spectate\fR
If netplay is used, it will go into a spectator mode.
Spectator mode allows one host to live stream game playback to multiple clients.
Essentially, clients receive a live streamed BSV movie file.
Clients can connect and disconnect at any time.
Clients thus cannot interact as player 2.
For spectating mode to work, both host and clients will need to use this flag.
.TP
\fB--ups PATCH, -U PATCH\fR
Attempts to apply an UPS patch to the current ROM image. No files are altered.

View File

@ -309,6 +309,7 @@ struct global
char netplay_server[PATH_MAX];
bool netplay_enable;
bool netplay_is_client;
bool netplay_is_spectate;
unsigned netplay_sync_frames;
uint16_t netplay_port;
#endif

55
movie.c
View File

@ -18,6 +18,7 @@
#include "movie.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "general.h"
#include "dynamic.h"
@ -327,3 +328,57 @@ void bsv_movie_frame_rewind(bsv_movie_t *handle)
fseek(handle->file, handle->min_file_pos, SEEK_SET);
}
}
uint8_t *bsv_header_generate(size_t *size)
{
uint32_t bsv_header[4] = {0};
unsigned serialize_size = psnes_serialize_size();
size_t header_size = sizeof(bsv_header) + serialize_size;
*size = header_size;
uint8_t *header = (uint8_t*)malloc(header_size);
if (!header)
return NULL;
bsv_header[MAGIC_INDEX] = swap_if_little32(BSV_MAGIC);
bsv_header[CRC_INDEX] = swap_if_big32(g_extern.cart_crc);
bsv_header[STATE_SIZE_INDEX] = swap_if_big32(serialize_size);
if (serialize_size && !psnes_serialize(header + sizeof(bsv_header), serialize_size))
{
free(header);
return NULL;
}
memcpy(header, bsv_header, sizeof(bsv_header));
return header;
}
bool bsv_parse_header(const uint32_t *header)
{
uint32_t in_bsv = swap_if_little32(header[MAGIC_INDEX]);
if (in_bsv != BSV_MAGIC)
{
SSNES_ERR("BSV magic mismatch, got 0x%x, expected 0x%x!\n",
in_bsv, BSV_MAGIC);
return false;
}
uint32_t in_crc = swap_if_big32(header[CRC_INDEX]);
if (in_crc != g_extern.cart_crc)
{
SSNES_ERR("CRC32 mismatch, got 0x%x, expected 0x%x!\n", in_crc, g_extern.cart_crc);
return false;
}
uint32_t in_state_size = swap_if_big32(header[STATE_SIZE_INDEX]);
if (in_state_size != psnes_serialize_size())
{
SSNES_ERR("Serialization size mismatch, got 0x%x, expected 0x%x!\n",
in_state_size, psnes_serialize_size());
return false;
}
return true;
}

View File

@ -19,6 +19,7 @@
#define __SSNES_MOVIE_H
#include <stdint.h>
#include <stddef.h>
#include "boolean.h"
typedef struct bsv_movie bsv_movie_t;
@ -29,6 +30,9 @@ enum ssnes_movie_type
SSNES_MOVIE_RECORD
};
uint8_t *bsv_header_generate(size_t *size);
bool bsv_parse_header(const uint32_t *header);
bsv_movie_t *bsv_movie_init(const char *path, enum ssnes_movie_type type);
// Playback

351
netplay.c
View File

@ -42,6 +42,18 @@
#include <stdlib.h>
#include <string.h>
// Checks if input port/index is controlled by netplay or not.
static bool netplay_is_alive(netplay_t *handle);
static bool netplay_poll(netplay_t *handle);
static int16_t netplay_input_state(netplay_t *handle, bool port, unsigned device, unsigned index, unsigned id);
// If we're fast-forward replaying to resync, check if we should actually show frame.
static bool netplay_should_skip(netplay_t *handle);
static bool netplay_can_poll(netplay_t *handle);
static const struct snes_callbacks* netplay_callbacks(netplay_t *handle);
static void netplay_set_spectate_input(netplay_t *handle, int16_t input);
#ifdef _WIN32
// Woohoo, Winsock has headers from the STONE AGE! :D
#define close(x) closesocket(x)
@ -69,6 +81,7 @@ struct delta_frame
};
#define UDP_FRAME_PACKETS 16
#define MAX_SPECTATORS 16
struct netplay
{
@ -100,6 +113,14 @@ struct netplay
bool has_client_addr;
unsigned timeout_cnt;
// Spectating.
bool spectate;
bool spectate_client;
int spectate_fds[MAX_SPECTATORS];
uint16_t *spectate_input;
size_t spectate_input_ptr;
size_t spectate_input_size;
};
static void warn_hangup(void)
@ -135,7 +156,7 @@ int16_t input_state_net(bool port, unsigned device, unsigned index, unsigned id)
return netplay_callbacks(g_extern.netplay)->state_cb(port, device, index, id);
}
static bool init_tcp_socket(netplay_t *handle, const char *server, uint16_t port)
static bool init_tcp_socket(netplay_t *handle, const char *server, uint16_t port, bool spectate)
{
struct addrinfo hints, *res = NULL;
memset(&hints, 0, sizeof(hints));
@ -176,12 +197,27 @@ static bool init_tcp_socket(netplay_t *handle, const char *server, uint16_t port
return false;
}
}
else if (handle->spectate)
{
int yes = 1;
setsockopt(handle->fd, SOL_SOCKET, SO_REUSEADDR, CONST_CAST &yes, sizeof(int));
if (bind(handle->fd, res->ai_addr, res->ai_addrlen) < 0 ||
listen(handle->fd, MAX_SPECTATORS) < 0)
{
SSNES_ERR("Failed to bind socket.\n");
close(handle->fd);
freeaddrinfo(res);
return false;
}
}
else
{
int yes = 1;
setsockopt(handle->fd, SOL_SOCKET, SO_REUSEADDR, CONST_CAST &yes, sizeof(int));
if (bind(handle->fd, res->ai_addr, res->ai_addrlen) < 0 || listen(handle->fd, 1) < 0)
if (bind(handle->fd, res->ai_addr, res->ai_addrlen) < 0 ||
listen(handle->fd, 1) < 0)
{
SSNES_ERR("Failed to bind socket.\n");
close(handle->fd);
@ -265,10 +301,13 @@ static bool init_socket(netplay_t *handle, const char *server, uint16_t port)
signal(SIGPIPE, SIG_IGN); // Do not like SIGPIPE killing our app :(
#endif
if (!init_tcp_socket(handle, server, port))
return false;
if (!init_udp_socket(handle, server, port))
if (!init_tcp_socket(handle, server, port, handle->spectate))
return false;
if (!handle->spectate)
{
if (!init_udp_socket(handle, server, port))
return false;
}
return true;
}
@ -346,7 +385,7 @@ static bool get_info(netplay_t *handle)
// Send SRAM data to our Player 2 :)
const uint8_t *sram = psnes_get_memory_data(SNES_MEMORY_CARTRIDGE_RAM);
unsigned sram_size = psnes_get_memory_size(SNES_MEMORY_CARTRIDGE_RAM);
while (sram_size > 0)
while (sram_size)
{
ssize_t ret = send(handle->fd, CONST_CAST sram, sram_size, 0);
if (ret <= 0)
@ -361,6 +400,50 @@ static bool get_info(netplay_t *handle)
return true;
}
static bool get_info_spectate(netplay_t *handle)
{
uint32_t header[4];
if (recv(handle->fd, NONCONST_CAST header, sizeof(header), 0) != (ssize_t)sizeof(header))
{
SSNES_ERR("Cannot get header from host!\n");
return false;
}
unsigned save_state_size = psnes_serialize_size();
if (!bsv_parse_header(header))
{
SSNES_ERR("Received invalid BSV header from host!\n");
return false;
}
uint8_t *buf = (uint8_t*)malloc(save_state_size);
if (!buf)
return false;
size_t size = save_state_size;
uint8_t *tmp_buf = buf;
while (size)
{
ssize_t ret = recv(handle->fd, NONCONST_CAST tmp_buf, size, 0);
if (ret <= 0)
{
SSNES_ERR("Failed to receive save state from host!\n");
free(tmp_buf);
return false;
}
size -= ret;
tmp_buf += ret;
}
bool ret = true;
if (save_state_size)
ret = psnes_unserialize(buf, save_state_size);
free(buf);
return ret;
}
static void init_buffers(netplay_t *handle)
{
handle->buffer = (struct delta_frame*)calloc(handle->buffer_size, sizeof(*handle->buffer));
@ -372,8 +455,10 @@ static void init_buffers(netplay_t *handle)
}
}
netplay_t *netplay_new(const char *server, uint16_t port, unsigned frames, const struct snes_callbacks *cb)
netplay_t *netplay_new(const char *server, uint16_t port, unsigned frames, const struct snes_callbacks *cb, bool spectate)
{
(void)spectate;
if (frames > UDP_FRAME_PACKETS)
frames = UDP_FRAME_PACKETS;
@ -381,8 +466,12 @@ netplay_t *netplay_new(const char *server, uint16_t port, unsigned frames, const
if (!handle)
return NULL;
handle->fd = -1;
handle->udp_fd = -1;
handle->cbs = *cb;
handle->port = server ? 0 : 1;
handle->spectate = spectate;
handle->spectate_client = server != NULL;
if (!init_socket(handle, server, port))
{
@ -390,35 +479,50 @@ netplay_t *netplay_new(const char *server, uint16_t port, unsigned frames, const
return NULL;
}
if (server)
if (spectate)
{
if (!send_info(handle))
if (server)
{
close(handle->fd);
free(handle);
return NULL;
if (!get_info_spectate(handle))
goto error;
}
for (unsigned i = 0; i < MAX_SPECTATORS; i++)
handle->spectate_fds[i] = -1;
}
else
{
if (!get_info(handle))
if (server)
{
close(handle->fd);
free(handle);
return NULL;
if (!send_info(handle))
goto error;
}
else
{
if (!get_info(handle))
goto error;
}
handle->buffer_size = frames + 1;
init_buffers(handle);
handle->has_connection = true;
}
handle->buffer_size = frames + 1;
init_buffers(handle);
handle->has_connection = true;
return handle;
error:
if (handle->fd >= 0)
close(handle->fd);
if (handle->udp_fd >= 0)
close(handle->udp_fd);
free(handle);
return NULL;
}
bool netplay_is_alive(netplay_t *handle)
static bool netplay_is_alive(netplay_t *handle)
{
return handle->has_connection;
}
@ -571,7 +675,7 @@ static bool receive_data(netplay_t *handle, uint32_t *buffer, size_t size)
}
// Poll network to see if we have anything new. If our network buffer is full, we simply have to block for new input data.
bool netplay_poll(netplay_t *handle)
static bool netplay_poll(netplay_t *handle)
{
if (!handle->has_connection)
return false;
@ -665,28 +769,42 @@ int16_t netplay_input_state(netplay_t *handle, bool port, unsigned device, unsig
void netplay_free(netplay_t *handle)
{
close(handle->fd);
close(handle->udp_fd);
for (unsigned i = 0; i < handle->buffer_size; i++)
free(handle->buffer[i].state);
free(handle->buffer);
if (handle->spectate)
{
for (unsigned i = 0; i < MAX_SPECTATORS; i++)
if (handle->spectate_fds[i] >= 0)
close(handle->spectate_fds[i]);
free(handle->spectate_input);
}
else
{
close(handle->udp_fd);
for (unsigned i = 0; i < handle->buffer_size; i++)
free(handle->buffer[i].state);
free(handle->buffer);
}
if (handle->addr)
freeaddrinfo(handle->addr);
free(handle);
}
const struct snes_callbacks* netplay_callbacks(netplay_t *handle)
static const struct snes_callbacks* netplay_callbacks(netplay_t *handle)
{
return &handle->cbs;
}
bool netplay_should_skip(netplay_t *handle)
static bool netplay_should_skip(netplay_t *handle)
{
return handle->is_replay && handle->has_connection;
}
void netplay_pre_frame(netplay_t *handle)
static void netplay_pre_frame_net(netplay_t *handle)
{
psnes_serialize(handle->buffer[handle->self_ptr].state, handle->state_size);
handle->can_poll = true;
@ -694,8 +812,133 @@ void netplay_pre_frame(netplay_t *handle)
input_poll_net();
}
// Here we check if we have new input and replay from recorded input.
void netplay_post_frame(netplay_t *handle)
static inline uint16_t swap_if_big16(uint16_t input)
{
if (is_little_endian())
return input;
else
return (input << 8) | (input >> 8);
}
static void netplay_set_spectate_input(netplay_t *handle, int16_t input)
{
if (handle->spectate_input_ptr >= handle->spectate_input_size)
{
handle->spectate_input_size++;
handle->spectate_input_size *= 2;
handle->spectate_input = (uint16_t*)realloc(handle->spectate_input,
handle->spectate_input_size * sizeof(uint16_t));
}
handle->spectate_input[handle->spectate_input_ptr++] = swap_if_big16(input);
}
int16_t input_state_spectate(bool port, unsigned device, unsigned index, unsigned id)
{
int16_t res = netplay_callbacks(g_extern.netplay)->state_cb(port, device, index, id);
netplay_set_spectate_input(g_extern.netplay, res);
return res;
}
static int16_t netplay_get_spectate_input(netplay_t *handle, bool port, unsigned device, unsigned index, unsigned id)
{
int16_t inp;
if (recv(handle->fd, NONCONST_CAST &inp, sizeof(inp), 0) == (ssize_t)sizeof(inp))
return swap_if_big16(inp);
else
{
SSNES_ERR("Connection with host was cut!\n");
msg_queue_clear(g_extern.msg_queue);
msg_queue_push(g_extern.msg_queue, "Connection with host was cut!", 1, 180);
psnes_set_input_state(netplay_callbacks(g_extern.netplay)->state_cb);
return netplay_callbacks(g_extern.netplay)->state_cb(port, device, index, id);
}
}
int16_t input_state_spectate_client(bool port, unsigned device, unsigned index, unsigned id)
{
return netplay_get_spectate_input(g_extern.netplay, port, device, index, id);
}
static void netplay_pre_frame_spectate(netplay_t *handle)
{
if (handle->spectate_client)
return;
fd_set fds;
FD_ZERO(&fds);
FD_SET(handle->fd, &fds);
struct timeval tmp_tv = {0};
if (select(handle->fd + 1, &fds, NULL, NULL, &tmp_tv) <= 0)
return;
if (!FD_ISSET(handle->fd, &fds))
return;
int new_fd = accept(handle->fd, NULL, NULL);
if (new_fd < 0)
{
SSNES_ERR("Failed to accept incoming spectator!\n");
return;
}
int index = -1;
for (unsigned i = 0; i < MAX_SPECTATORS; i++)
{
if (handle->spectate_fds[i] == -1)
{
index = i;
break;
}
}
// No vacant client streams :(
if (index == -1)
{
close(new_fd);
return;
}
size_t header_size;
uint8_t *header = bsv_header_generate(&header_size);
if (!header)
{
SSNES_ERR("Failed to generate BSV header!\n");
close(new_fd);
return;
}
const uint8_t *tmp_header = header;
while (header_size)
{
ssize_t ret = send(new_fd, CONST_CAST tmp_header, header_size, 0);
if (ret <= 0)
{
SSNES_ERR("Failed to send header to client!\n");
close(new_fd);
free(header);
return;
}
header_size -= ret;
tmp_header += ret;
}
free(header);
handle->spectate_fds[index] = new_fd;
}
void netplay_pre_frame(netplay_t *handle)
{
if (handle->spectate)
netplay_pre_frame_spectate(handle);
else
netplay_pre_frame_net(handle);
}
static void netplay_post_frame_net(netplay_t *handle)
{
handle->frame_count++;
@ -706,7 +949,7 @@ void netplay_post_frame(netplay_t *handle)
// Skip ahead if we predicted correctly. Skip until our simulation failed.
while (handle->other_frame_count < handle->read_frame_count)
{
struct delta_frame *ptr = &handle->buffer[handle->other_ptr];
const struct delta_frame *ptr = &handle->buffer[handle->other_ptr];
if ((ptr->simulated_input_state != ptr->real_input_state) && !ptr->used_real)
break;
handle->other_ptr = NEXT_PTR(handle->other_ptr);
@ -739,3 +982,43 @@ void netplay_post_frame(netplay_t *handle)
}
}
static void netplay_post_frame_spectate(netplay_t *handle)
{
if (handle->spectate_client)
return;
for (unsigned i = 0; i < MAX_SPECTATORS; i++)
{
if (handle->spectate_fds[i] == -1)
continue;
size_t send_size = handle->spectate_input_ptr * sizeof(int16_t);
const uint8_t *tmp_buf = (const uint8_t*)handle->spectate_input;
while (send_size)
{
ssize_t ret = send(handle->spectate_fds[i], CONST_CAST tmp_buf, send_size, 0);
if (ret <= 0)
{
SSNES_LOG("Client disconnected ...\n");
close(handle->spectate_fds[i]);
handle->spectate_fds[i] = -1;
break;
}
tmp_buf += ret;
send_size -= ret;
}
}
handle->spectate_input_ptr = 0;
}
// Here we check if we have new input and replay from recorded input.
void netplay_post_frame(netplay_t *handle)
{
if (handle->spectate)
netplay_post_frame_spectate(handle);
else
netplay_post_frame_net(handle);
}

View File

@ -28,6 +28,9 @@ int16_t input_state_net(bool port, unsigned device, unsigned index, unsigned id)
void video_frame_net(const uint16_t *data, unsigned width, unsigned height);
void audio_sample_net(uint16_t left, uint16_t right);
int16_t input_state_spectate(bool port, unsigned device, unsigned index, unsigned id);
int16_t input_state_spectate_client(bool port, unsigned device, unsigned index, unsigned id);
typedef struct netplay netplay_t;
struct snes_callbacks
@ -38,7 +41,7 @@ struct snes_callbacks
};
// Creates a new netplay handle. A NULL host means we're hosting (player 1). :)
netplay_t *netplay_new(const char *server, uint16_t port, unsigned frames, const struct snes_callbacks *cb);
netplay_t *netplay_new(const char *server, uint16_t port, unsigned frames, const struct snes_callbacks *cb, bool spectate);
void netplay_free(netplay_t *handle);
// Call this before running snes_run()
@ -46,15 +49,4 @@ void netplay_pre_frame(netplay_t *handle);
// Call this after running snes_run()
void netplay_post_frame(netplay_t *handle);
// Checks if input port/index is controlled by netplay or not.
bool netplay_is_alive(netplay_t *handle);
bool netplay_poll(netplay_t *handle);
int16_t netplay_input_state(netplay_t *handle, bool port, unsigned device, unsigned index, unsigned id);
// If we're fast-forward replaying to resync, check if we should actually show frame.
bool netplay_should_skip(netplay_t *handle);
bool netplay_can_poll(netplay_t *handle);
const struct snes_callbacks* netplay_callbacks(netplay_t *handle);
#endif

55
ssnes.c
View File

@ -492,6 +492,9 @@ static void print_help(void)
puts("\t-C/--connect: Connect to netplay as player 2.");
puts("\t--port: Port used to netplay. Default is 55435.");
puts("\t-F/--frames: Sync frames when using netplay.");
puts("\t--spectate: Netplay will become spectacting mode.");
puts("\t\tHost can live stream the game content to players that connect.");
puts("\t\tHowever, the client will not be able to play. Multiple clients can connect to the host.");
#endif
#ifdef HAVE_FFMPEG
@ -640,6 +643,7 @@ static void parse_input(int argc, char *argv[])
{ "connect", 1, NULL, 'C' },
{ "frames", 1, NULL, 'F' },
{ "port", 1, &val, 'p' },
{ "spectate", 0, &val, 'S' },
#endif
{ "ups", 1, NULL, 'U' },
{ "bps", 1, &val, 'B' },
@ -854,6 +858,10 @@ static void parse_input(int argc, char *argv[])
case 'p':
g_extern.netplay_port = strtoul(optarg, NULL, 0);
break;
case 'S':
g_extern.netplay_is_spectate = true;
break;
#endif
case 'B':
@ -1221,7 +1229,7 @@ static void init_netplay(void)
g_extern.netplay = netplay_new(g_extern.netplay_is_client ? g_extern.netplay_server : NULL,
g_extern.netplay_port ? g_extern.netplay_port : SSNES_DEFAULT_PORT,
g_extern.netplay_sync_frames, &cbs);
g_extern.netplay_sync_frames, &cbs, g_extern.netplay_is_spectate);
if (!g_extern.netplay)
{
@ -1236,9 +1244,7 @@ static void init_netplay(void)
}
}
}
#endif
#ifdef HAVE_NETPLAY
static void deinit_netplay(void)
{
if (g_extern.netplay)
@ -1246,6 +1252,34 @@ static void deinit_netplay(void)
}
#endif
static void init_libsnes_cbs(void)
{
#ifdef HAVE_NETPLAY
if (g_extern.netplay)
{
psnes_set_video_refresh(g_extern.netplay_is_spectate ?
video_frame : video_frame_net);
psnes_set_audio_sample(g_extern.netplay_is_spectate ?
audio_sample : audio_sample_net);
psnes_set_input_state(g_extern.netplay_is_spectate ?
(g_extern.netplay_is_client ? input_state_spectate_client : input_state_spectate)
: input_state_net);
}
else
{
psnes_set_video_refresh(video_frame);
psnes_set_audio_sample(audio_sample);
psnes_set_input_state(input_state);
}
#else
psnes_set_video_refresh(video_frame);
psnes_set_audio_sample(audio_sample);
psnes_set_input_state(input_state);
#endif
psnes_set_input_poll(input_poll);
}
static void init_autosave(void)
{
#ifdef HAVE_THREADS
@ -2058,20 +2092,7 @@ int main(int argc, char *argv[])
#endif
init_rewind();
#ifdef HAVE_NETPLAY
psnes_set_video_refresh(g_extern.netplay ?
video_frame_net : video_frame);
psnes_set_audio_sample(g_extern.netplay ?
audio_sample_net : audio_sample);
psnes_set_input_state(g_extern.netplay ?
input_state_net : input_state);
#else
psnes_set_video_refresh(video_frame);
psnes_set_audio_sample(audio_sample);
psnes_set_input_state(input_state);
#endif
psnes_set_input_poll(input_poll);
init_libsnes_cbs();
init_controllers();
#ifdef HAVE_FFMPEG