mupen64plus-core/src/main/netplay.c
Logan McNaughton 1fccc3ba6c Netplay
2020-06-06 16:09:45 -06:00

670 lines
22 KiB
C

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Mupen64plus - netplay.c *
* Mupen64Plus homepage: https://mupen64plus.org/ *
* Copyright (C) 2020 loganmc10 *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#define M64P_CORE_PROTOTYPES 1
#include "api/callbacks.h"
#include "main.h"
#include "util.h"
#include "plugin/plugin.h"
#include "backends/plugins_compat/plugins_compat.h"
#include "netplay.h"
#include <SDL_net.h>
static int l_canFF;
static int l_netplay_controller;
static int l_netplay_control[4];
static UDPsocket l_udpSocket;
static TCPsocket l_tcpSocket;
static int l_udpChannel;
static int l_spectator;
static int l_netplay_is_init = 0;
static uint32_t l_vi_counter;
static uint8_t l_status;
static uint32_t l_reg_id;
static struct controller_input_compat *l_cin_compats;
static uint8_t l_plugin[4];
static uint8_t l_buffer_target;
static uint8_t l_player_lag[4];
//UDP packet formats
#define UDP_SEND_KEY_INFO 0
#define UDP_RECEIVE_KEY_INFO 1
#define UDP_REQUEST_KEY_INFO 2
#define UDP_SYNC_DATA 4
//TCP packet formats
#define TCP_SEND_SAVE 1
#define TCP_RECEIVE_SAVE 2
#define TCP_SEND_SETTINGS 3
#define TCP_RECEIVE_SETTINGS 4
#define TCP_REGISTER_PLAYER 5
#define TCP_GET_REGISTRATION 6
#define TCP_DISCONNECT_NOTICE 7
m64p_error netplay_start(const char* host, int port)
{
if (SDLNet_Init() < 0)
{
DebugMessage(M64MSG_ERROR, "Netplay: Could not initialize SDL Net library");
return M64ERR_SYSTEM_FAIL;
}
l_udpSocket = SDLNet_UDP_Open(0);
if (l_udpSocket == NULL)
{
DebugMessage(M64MSG_ERROR, "Netplay: UDP socket creation failed");
return M64ERR_SYSTEM_FAIL;
}
IPaddress dest;
SDLNet_ResolveHost(&dest, host, port);
l_udpChannel = SDLNet_UDP_Bind(l_udpSocket, -1, &dest);
if (l_udpChannel < 0)
{
DebugMessage(M64MSG_ERROR, "Netplay: could not bind to UDP socket");
SDLNet_UDP_Close(l_udpSocket);
l_udpSocket = NULL;
return M64ERR_SYSTEM_FAIL;
}
l_tcpSocket = SDLNet_TCP_Open(&dest);
if (l_tcpSocket == NULL)
{
DebugMessage(M64MSG_ERROR, "Netplay: could not bind to TCP socket");
SDLNet_UDP_Close(l_udpSocket);
l_udpSocket = NULL;
return M64ERR_SYSTEM_FAIL;
}
for (int i = 0; i < 4; ++i)
{
l_netplay_control[i] = -1;
l_plugin[i] = 0;
l_player_lag[i] = 0;
}
l_canFF = 0;
l_netplay_controller = 0;
l_netplay_is_init = 1;
l_spectator = 1;
l_vi_counter = 0;
l_status = 0;
l_reg_id = 0;
return M64ERR_SUCCESS;
}
m64p_error netplay_stop()
{
if (l_udpSocket == NULL)
return M64ERR_INVALID_STATE;
else
{
char output_data[5];
output_data[0] = TCP_DISCONNECT_NOTICE;
SDLNet_Write32(l_reg_id, &output_data[1]);
SDLNet_TCP_Send(l_tcpSocket, &output_data[0], 5);
SDLNet_UDP_Unbind(l_udpSocket, l_udpChannel);
SDLNet_UDP_Close(l_udpSocket);
SDLNet_TCP_Close(l_tcpSocket);
l_tcpSocket = NULL;
l_udpSocket = NULL;
l_udpChannel = -1;
l_netplay_is_init = 0;
SDLNet_Quit();
return M64ERR_SUCCESS;
}
}
int netplay_is_init()
{
return l_netplay_is_init;
}
static uint8_t buffer_size(uint8_t control_id)
{
//This function returns the size of the local input buffer
uint8_t counter = 0;
struct netplay_event* current = l_cin_compats[control_id].event_first;
while (current != NULL)
{
current = current->next;
++counter;
}
return counter;
}
static void netplay_request_input(uint8_t control_id)
{
UDPpacket *packet = SDLNet_AllocPacket(12);
packet->data[0] = UDP_REQUEST_KEY_INFO;
packet->data[1] = control_id; //The player we need input for
SDLNet_Write32(l_reg_id, &packet->data[2]); //our registration ID
SDLNet_Write32(l_cin_compats[control_id].netplay_count, &packet->data[6]); //the current event count
packet->data[10] = l_spectator; //whether we are a spectator
packet->data[11] = buffer_size(control_id); //our local buffer size
packet->len = 12;
SDLNet_UDP_Send(l_udpSocket, l_udpChannel, packet);
SDLNet_FreePacket(packet);
}
static int check_valid(uint8_t control_id, uint32_t count)
{
//Check if we already have this event recorded locally, returns 1 if we do
struct netplay_event* current = l_cin_compats[control_id].event_first;
while (current != NULL)
{
if (current->count == count) //event already recorded
return 1;
current = current->next;
}
return 0;
}
static int netplay_require_response(void* opaque)
{
//This function runs inside a thread.
//It runs if our local buffer size is 0 (we need to execute a key event, but we don't have the data we need).
//We basically beg the server for input data.
//After 10 seconds a timeout occurs, we assume we have lost connection to the server.
uint8_t control_id = *(uint8_t*)opaque;
uint32_t timeout = SDL_GetTicks() + 10000;
while (!check_valid(control_id, l_cin_compats[control_id].netplay_count))
{
if (SDL_GetTicks() > timeout)
{
l_udpChannel = -1;
return 0;
}
netplay_request_input(control_id);
SDL_Delay(5);
}
return 1;
}
static void netplay_process()
{
//In this function we process data we have received from the server
UDPpacket *packet = SDLNet_AllocPacket(512);
uint32_t curr, count, keys;
uint8_t plugin, player, current_status;
while (SDLNet_UDP_Recv(l_udpSocket, packet) == 1)
{
switch (packet->data[0])
{
case UDP_RECEIVE_KEY_INFO:
player = packet->data[1];
//current_status is a status update from the server
//it will let us know if another player has disconnected, or the games have desynced
current_status = packet->data[2];
l_player_lag[player] = packet->data[3];
if (current_status != l_status)
{
if (((current_status & 0x1) ^ (l_status & 0x1)) != 0)
DebugMessage(M64MSG_ERROR, "Netplay: players have de-synced at VI %u", l_vi_counter);
for (int dis = 1; dis < 5; ++dis)
{
if (((current_status & (0x1 << dis)) ^ (l_status & (0x1 << dis))) != 0)
DebugMessage(M64MSG_ERROR, "Netplay: player %u has disconnected", dis);
}
l_status = current_status;
}
curr = 5;
//this loop processes input data from the server, inserting new events into the linked list for each player
//it skips events that we have already recorded, or if we receive data for an event that has already happened
for (uint8_t i = 0; i < packet->data[4]; ++i)
{
count = SDLNet_Read32(&packet->data[curr]);
curr += 4;
if (((count - l_cin_compats[player].netplay_count) > (UINT32_MAX / 2)) || (check_valid(player, count))) //event doesn't need to be recorded
{
curr += 5;
continue;
}
keys = SDLNet_Read32(&packet->data[curr]);
curr += 4;
plugin = packet->data[curr];
curr += 1;
//insert new event at beginning of linked list
struct netplay_event* new_event = (struct netplay_event*)malloc(sizeof(struct netplay_event));
new_event->count = count;
new_event->buttons = keys;
new_event->plugin = plugin;
new_event->next = l_cin_compats[player].event_first;
l_cin_compats[player].event_first = new_event;
}
break;
default:
DebugMessage(M64MSG_ERROR, "Netplay: received unknown message from server");
break;
}
}
SDLNet_FreePacket(packet);
}
static int netplay_ensure_valid(uint8_t control_id)
{
//This function makes sure we have data for a certain event
//If we don't have the data, it will create a new thread that will request the data
if (check_valid(control_id, l_cin_compats[control_id].netplay_count))
return 1;
if (l_udpChannel == -1)
return 0;
#if SDL_VERSION_ATLEAST(2,0,0)
SDL_Thread* thread = SDL_CreateThread(netplay_require_response, "Netplay key request", &control_id);
#else
SDL_Thread* thread = SDL_CreateThread(netplay_require_response, &control_id);
#endif
while (!check_valid(control_id, l_cin_compats[control_id].netplay_count) && l_udpChannel != -1)
netplay_process();
int success;
SDL_WaitThread(thread, &success);
return success;
}
static void netplay_delete_event(struct netplay_event* current, uint8_t control_id)
{
//This function deletes an event from the linked list
struct netplay_event* find = l_cin_compats[control_id].event_first;
while (find != NULL)
{
if (find->next == current)
{
find->next = current->next;
break;
}
find = find->next;
}
if (current == l_cin_compats[control_id].event_first)
l_cin_compats[control_id].event_first = l_cin_compats[control_id].event_first->next;
free(current);
}
static uint32_t netplay_get_input(uint8_t control_id)
{
uint32_t keys;
netplay_process();
netplay_request_input(control_id);
//l_buffer_target is set by the server upon registration
//l_player_lag is how far behind we are from the lead player
//buffer_size is the local buffer size
if (l_player_lag[control_id] > 0 && buffer_size(control_id) > l_buffer_target)
{
l_canFF = 1;
main_core_state_set(M64CORE_SPEED_LIMITER, 0);
}
else
{
main_core_state_set(M64CORE_SPEED_LIMITER, 1);
l_canFF = 0;
}
if (netplay_ensure_valid(control_id))
{
//We grab the event from the linked list, the delete it once it has been used
//Finally we increment the event counter
struct netplay_event* current = l_cin_compats[control_id].event_first;
while (current->count != l_cin_compats[control_id].netplay_count)
current = current->next;
keys = current->buttons;
Controls[control_id].Plugin = current->plugin;
netplay_delete_event(current, control_id);
++l_cin_compats[control_id].netplay_count;
}
else
{
DebugMessage(M64MSG_ERROR, "Netplay: lost connection to server");
main_core_state_set(M64CORE_EMU_STATE, M64EMU_STOPPED);
keys = 0;
}
return keys;
}
static void netplay_send_input(uint8_t control_id, uint32_t keys)
{
UDPpacket *packet = SDLNet_AllocPacket(11);
packet->data[0] = UDP_SEND_KEY_INFO;
packet->data[1] = control_id; //player number
SDLNet_Write32(l_cin_compats[control_id].netplay_count, &packet->data[2]); // current event count
SDLNet_Write32(keys, &packet->data[6]); //key data
packet->data[10] = l_plugin[control_id]; //current plugin
packet->len = 11;
SDLNet_UDP_Send(l_udpSocket, l_udpChannel, packet);
SDLNet_FreePacket(packet);
}
uint8_t netplay_register_player(uint8_t player, uint8_t plugin, uint8_t rawdata, uint32_t reg_id)
{
l_reg_id = reg_id;
char output_data[8];
output_data[0] = TCP_REGISTER_PLAYER;
output_data[1] = player; //player number we'd like to register
output_data[2] = plugin; //current plugin
output_data[3] = rawdata; //whether we are using a RawData input plugin
SDLNet_Write32(l_reg_id, &output_data[4]);
SDLNet_TCP_Send(l_tcpSocket, &output_data[0], 8);
uint8_t response[2];
size_t recv = 0;
while (recv < 2)
recv += SDLNet_TCP_Recv(l_tcpSocket, &response[recv], 2 - recv);
l_buffer_target = response[1]; //local buffer size target
return response[0];
}
int netplay_lag()
{
return l_canFF;
}
int netplay_next_controller()
{
return l_netplay_controller;
}
void netplay_set_controller(uint8_t player)
{
l_netplay_control[player] = l_netplay_controller++;
l_spectator = 0;
}
int netplay_get_controller(uint8_t player)
{
return l_netplay_control[player];
}
file_status_t netplay_read_storage(const char *filename, void *data, size_t size)
{
//This function syncs save games.
//If the client is controlling player 1, it sends its save game to the server
//All other players receive save files from the server
const char *short_filename = strrchr(filename, '/');
if (short_filename == NULL)
short_filename = strrchr(filename, '\\');
short_filename += 1;
uint32_t buffer_pos = 0;
char *output_data = malloc(size + strlen(short_filename) + 6);
file_status_t ret;
uint8_t request;
if (l_netplay_control[0] != -1)
{
request = TCP_SEND_SAVE;
memcpy(&output_data[buffer_pos], &request, 1);
++buffer_pos;
//send file name
memcpy(&output_data[buffer_pos], short_filename, strlen(short_filename) + 1);
buffer_pos += strlen(short_filename) + 1;
ret = read_from_file(filename, data, size);
if (ret == file_open_error)
memset(data, 0, size); //all zeros means there is no save file
SDLNet_Write32((int32_t)size, &output_data[buffer_pos]); //file data size
buffer_pos += 4;
memcpy(&output_data[buffer_pos], data, size); //file data
buffer_pos += size;
SDLNet_TCP_Send(l_tcpSocket, &output_data[0], buffer_pos);
}
else
{
request = TCP_RECEIVE_SAVE;
memcpy(&output_data[buffer_pos], &request, 1);
++buffer_pos;
//name of the file we are requesting
memcpy(&output_data[buffer_pos], short_filename, strlen(short_filename) + 1);
buffer_pos += strlen(short_filename) + 1;
SDLNet_TCP_Send(l_tcpSocket, &output_data[0], buffer_pos);
size_t recv = 0;
char *data_array = data;
while (recv < size)
recv += SDLNet_TCP_Recv(l_tcpSocket, data_array + recv, size - recv);
int sum = 0;
for (int i = 0; i < size; ++i)
sum |= data_array[i];
if (sum == 0) //all zeros means there is no save file
ret = file_open_error;
else
ret = file_ok;
}
free(output_data);
return ret;
}
void netplay_sync_settings(uint32_t *count_per_op, uint32_t *disable_extra_mem, int32_t *si_dma_duration, uint32_t *emumode, int32_t *no_compiled_jump)
{
if (!netplay_is_init())
return;
char output_data[21];
uint8_t request;
if (l_netplay_control[0] != -1) //player 1 is the source of truth for settings
{
request = TCP_SEND_SETTINGS;
memcpy(&output_data[0], &request, 1);
SDLNet_Write32(*count_per_op, &output_data[1]);
SDLNet_Write32(*disable_extra_mem, &output_data[5]);
SDLNet_Write32(*si_dma_duration, &output_data[9]);
SDLNet_Write32(*emumode, &output_data[13]);
SDLNet_Write32(*no_compiled_jump, &output_data[17]);
SDLNet_TCP_Send(l_tcpSocket, &output_data[0], 21);
}
else
{
request = TCP_RECEIVE_SETTINGS;
memcpy(&output_data[0], &request, 1);
SDLNet_TCP_Send(l_tcpSocket, &output_data[0], 1);
int32_t recv = 0;
while (recv < 20)
recv += SDLNet_TCP_Recv(l_tcpSocket, &output_data[recv], 20 - recv);
*count_per_op = SDLNet_Read32(&output_data[0]);
*disable_extra_mem = SDLNet_Read32(&output_data[4]);
*si_dma_duration = SDLNet_Read32(&output_data[8]);
*emumode = SDLNet_Read32(&output_data[12]);
*no_compiled_jump = SDLNet_Read32(&output_data[16]);
}
}
void netplay_check_sync(struct cp0* cp0)
{
//This function is used to check if games have desynced
//Every 60 VIs, it sends the value of the CP0 registers to the server
//The server will compare the values, and update the status byte if it detects a desync
if (!netplay_is_init())
return;
const uint32_t* cp0_regs = r4300_cp0_regs(cp0);
if (l_vi_counter % 60 == 0)
{
uint32_t packet_len = (CP0_REGS_COUNT * 4) + 5;
UDPpacket *packet = SDLNet_AllocPacket(packet_len);
packet->data[0] = UDP_SYNC_DATA;
SDLNet_Write32(l_vi_counter, &packet->data[1]); //current VI count
for (int i = 0; i < CP0_REGS_COUNT; ++i)
{
SDLNet_Write32(cp0_regs[i], &packet->data[(i * 4) + 5]);
}
packet->len = packet_len;
SDLNet_UDP_Send(l_udpSocket, l_udpChannel, packet);
SDLNet_FreePacket(packet);
}
++l_vi_counter;
}
void netplay_read_registration(struct controller_input_compat* cin_compats)
{
//This function runs right before the game starts
//The server shares the registration details about each player
if (!netplay_is_init())
return;
l_cin_compats = cin_compats;
uint32_t reg_id;
char output_data = TCP_GET_REGISTRATION;
char input_data[24];
SDLNet_TCP_Send(l_tcpSocket, &output_data, 1);
size_t recv = 0;
while (recv < 24)
recv += SDLNet_TCP_Recv(l_tcpSocket, &input_data[recv], 24 - recv);
uint32_t curr = 0;
for (int i = 0; i < 4; ++i)
{
reg_id = SDLNet_Read32(&input_data[curr]);
curr += 4;
if (reg_id == 0) //No one registered to control this player
{
Controls[i].Present = 0;
Controls[i].Plugin = 1;
Controls[i].RawData = 0;
curr += 2;
}
else
{
Controls[i].Present = 1;
Controls[i].Plugin = input_data[curr];
l_plugin[i] = Controls[i].Plugin;
++curr;
Controls[i].RawData = input_data[curr];
++curr;
}
}
}
static void netplay_send_raw_input(struct pif* pif)
{
for (int i = 0; i < 4; ++i)
{
if (l_netplay_control[i] != -1)
{
if (pif->channels[i].tx && pif->channels[i].tx_buf[0] == JCMD_CONTROLLER_READ)
netplay_send_input(i, *(uint32_t*)pif->channels[i].rx_buf);
}
}
}
static void netplay_get_raw_input(struct pif* pif)
{
for (int i = 0; i < 4; ++i)
{
if (Controls[i].Present == 1)
{
if (pif->channels[i].tx)
{
*pif->channels[i].rx &= ~0xC0; //Always show the controller as connected
if(pif->channels[i].tx_buf[0] == JCMD_CONTROLLER_READ)
{
*(uint32_t*)pif->channels[i].rx_buf = netplay_get_input(i);
}
else if ((pif->channels[i].tx_buf[0] == JCMD_STATUS || pif->channels[i].tx_buf[0] == JCMD_RESET) && Controls[i].RawData)
{
//a bit of a hack for raw input controllers, force the status
uint16_t type = JDT_JOY_ABS_COUNTERS | JDT_JOY_PORT;
pif->channels[i].rx_buf[0] = (uint8_t)(type >> 0);
pif->channels[i].rx_buf[1] = (uint8_t)(type >> 8);
pif->channels[i].rx_buf[2] = 0;
}
else if (pif->channels[i].tx_buf[0] == JCMD_PAK_READ && Controls[i].RawData)
{
//also a hack for raw input, we return "mempak not present" if the game tries to read the mempak
pif->channels[i].rx_buf[32] = 255;
}
else if (pif->channels[i].tx_buf[0] == JCMD_PAK_WRITE && Controls[i].RawData)
{
//also a hack for raw input, we return "mempak not present" if the game tries to write to mempak
pif->channels[i].rx_buf[0] = 255;
}
}
}
}
}
void netplay_update_input(struct pif* pif)
{
if (netplay_is_init())
{
netplay_send_raw_input(pif);
netplay_get_raw_input(pif);
}
}
void netplay_set_plugin(uint8_t control_id, uint8_t plugin)
{
if (!(control_id > 0 && plugin == 2)) //Only P1 can use mempak
l_plugin[control_id] = plugin;
}
m64p_error netplay_send_config(char* data, int size)
{
if (!netplay_is_init())
return M64ERR_NOT_INIT;
if (l_netplay_control[0] != -1 || size == 1) //Only P1 sends settings, we allow all players to send if the size is 1, this may be a request packet
{
int result = SDLNet_TCP_Send(l_tcpSocket, data, size);
if (result < size)
return M64ERR_SYSTEM_FAIL;
return M64ERR_SUCCESS;
}
else
return M64ERR_INVALID_STATE;
}
m64p_error netplay_receive_config(char* data, int size)
{
if (!netplay_is_init())
return M64ERR_NOT_INIT;
if (l_netplay_control[0] == -1) //Only P2-4 receive settings
{
int recv = 0;
while (recv < size)
{
recv += SDLNet_TCP_Recv(l_tcpSocket, &data[recv], size - recv);
if (recv < 1)
return M64ERR_SYSTEM_FAIL;
}
return M64ERR_SUCCESS;
}
else
return M64ERR_INVALID_STATE;
}