From 4beec3870ffddc60984c5d70b894436021a133e6 Mon Sep 17 00:00:00 2001 From: Themaister Date: Wed, 11 Jan 2012 19:22:18 +0100 Subject: [PATCH] Spectator mode. --- docs/ssnes.1 | 9 ++ general.h | 1 + movie.c | 55 ++++++++ movie.h | 4 + netplay.c | 351 ++++++++++++++++++++++++++++++++++++++++++++++----- netplay.h | 16 +-- ssnes.c | 55 +++++--- 7 files changed, 428 insertions(+), 63 deletions(-) diff --git a/docs/ssnes.1 b/docs/ssnes.1 index ee5fb4bcbe..bd24fce492 100644 --- a/docs/ssnes.1 +++ b/docs/ssnes.1 @@ -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. diff --git a/general.h b/general.h index 3f18597bcb..abacec8e63 100644 --- a/general.h +++ b/general.h @@ -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 diff --git a/movie.c b/movie.c index 8920df5a71..f771142152 100644 --- a/movie.c +++ b/movie.c @@ -18,6 +18,7 @@ #include "movie.h" #include #include +#include #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; +} + diff --git a/movie.h b/movie.h index 5a95b1ac56..f3ff48dfe6 100644 --- a/movie.h +++ b/movie.h @@ -19,6 +19,7 @@ #define __SSNES_MOVIE_H #include +#include #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 diff --git a/netplay.c b/netplay.c index bb3ff73a39..92706f596c 100644 --- a/netplay.c +++ b/netplay.c @@ -42,6 +42,18 @@ #include #include +// 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); +} + diff --git a/netplay.h b/netplay.h index 3bbcf06f4d..4b5466aea0 100644 --- a/netplay.h +++ b/netplay.h @@ -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 diff --git a/ssnes.c b/ssnes.c index 86545cb561..41e5927aa8 100644 --- a/ssnes.c +++ b/ssnes.c @@ -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