From 4d59723cc23b4097b109ea7bf5406b5ccdecdd03 Mon Sep 17 00:00:00 2001 From: zicodxx <> Date: Sat, 31 Jan 2009 23:47:15 +0000 Subject: [PATCH] Added Packet-loss prevention code for Netgames --- CHANGELOG.txt | 5 + SConstruct | 1 + main/multi.c | 2 +- main/multi.h | 2 +- main/netdrv_udp.c | 19 +++- main/netdrv_udp.h | 6 +- main/network.c | 27 +++++- main/network.h | 1 + main/noloss.c | 227 ++++++++++++++++++++++++++++++++++++++++++++++ main/noloss.h | 42 +++++++++ 10 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 main/noloss.c create mode 100644 main/noloss.h diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b64b72898..4c3fdd8b2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,10 @@ D2X-Rebirth Changelog +20090201 +-------- +main/netdrv_udp.c: Fixing bug with incorrect/missing UDP port termination +main/network.c, main/network.h, main/multi.h, main/netdrv_udp.c, main/netdrv_udp.h, main/noloss.c, main/noloss.h, SConstruct: Added Packet-loss prevention code for Netgames + 20090130 -------- arch/ogl/ogl.c, main/render.c: New level render order for better seperation and blending between transculent level geometry and sprites diff --git a/SConstruct b/SConstruct index 1ff3b281e..4aee56bd2 100644 --- a/SConstruct +++ b/SConstruct @@ -141,6 +141,7 @@ common_sources = [ 'main/network.c', 'main/newdemo.c', 'main/newmenu.c', +'main/noloss.c', 'main/object.c', 'main/paging.c', 'main/physics.c', diff --git a/main/multi.c b/main/multi.c index 27756e8df..71766a5d0 100644 --- a/main/multi.c +++ b/main/multi.c @@ -2708,7 +2708,7 @@ multi_send_remobj(int objnum) multibuf[3] = obj_owner; - multi_send_data(multibuf, 4, 0); + multi_send_data(multibuf, 4, 1); if (Network_send_objects && network_objnum_is_past(objnum)) { diff --git a/main/multi.h b/main/multi.h index 98d11b097..364ba3949 100644 --- a/main/multi.h +++ b/main/multi.h @@ -35,7 +35,7 @@ COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED. #else #define MULTI_PROTO_VERSION 4 #endif -#define MULTI_PROTO_D2X_VER 6 // Increment everytime we change networking features +#define MULTI_PROTO_D2X_VER 7 // Increment everytime we change networking features - based on ubyte, must never be > 255 // Protocol versions: // 1 Descent Shareware diff --git a/main/netdrv_udp.c b/main/netdrv_udp.c index a338f9229..9b8c4bfea 100755 --- a/main/netdrv_udp.c +++ b/main/netdrv_udp.c @@ -271,6 +271,16 @@ void udp_packet_relay(char *text, int len, struct _sockaddr *sAddr) // Never relay PING packets if (text[0] == PID_PING_SEND || text[0] == PID_PING_RETURN) return; + + // We got an PID_PDATA_ACK packet - check if it is for me (host). If not, transfer it to designated client if sender or receiver is relay client. + if (text[0] == PID_PDATA_ACK) + { + if ((int)text[2] != Player_num && (UDP_Peers[NetPlayers.players[(int)text[1]].network.ipx.node[0]].relay || UDP_Peers[NetPlayers.players[(int)text[2]].network.ipx.node[0]].relay)) + { + sendto (UDP_sock, text, len, 0, (struct sockaddr *) &UDP_Peers[NetPlayers.players[(int)text[2]].network.ipx.node[0]].addr, sizeof(struct _sockaddr)); + } + return; + } // Check if sender is a relay client and store ID if so for (i = 1; i < MAX_CONNECTIONS; i++) @@ -567,7 +577,14 @@ static int udp_send_packet(socket_t *unused, IPXPacket_t *IPXHeader, ubyte *text return 0; if (!UDP_Peers[IPXHeader->Destination.Node[0]].valid) - return 0; + { + // PID_PDATA_ACK needs to be delivered to designated peer. If there's a relay player, Host cannot relay, as PID_PDATA_ACK is sent specifically. + // So in this case, if we encounter invalid ID while PID_PDATA_ACK, wrap this packet to Host so it can be relayed. + if (text[0] == PID_PDATA_ACK) + return sendto (UDP_sock, text, len, 0, (struct sockaddr *) &UDP_Peers[0].addr, sizeof(struct _sockaddr)); + else + return 0; + } return sendto (UDP_sock, text, len, 0, (struct sockaddr *) &UDP_Peers[IPXHeader->Destination.Node[0]].addr, sizeof(struct _sockaddr)); } diff --git a/main/netdrv_udp.h b/main/netdrv_udp.h index 5140c9030..76237469f 100644 --- a/main/netdrv_udp.h +++ b/main/netdrv_udp.h @@ -38,7 +38,7 @@ #endif #define DXXcfgid "D2Rc" // identification string for UDP/IP configuration packets -#define MAX_CONNECTIONS 32 // maximum connections that can be stored in UDPPeers +#define MAX_CONNECTIONS 32 // maximum connections that can be stored in UDPPeers - as this currently bases on player node[0] this must NOT exceed ubyte! #define LEN_SERVERNAME 41 #define LEN_PORT 6 #define UDP_BASEPORT 31017 @@ -54,11 +54,11 @@ typedef struct peer_list { struct _sockaddr addr; // real address information about this peer - int valid; // 1 = client connected / 2 = client ready for handshaking / 3 = client done with handshake and fully joined + int valid; // 1 = client connected / 2 = client ready for handshaking / 3 = client done with handshake and fully joined / 0 between clients = no connection -> relay fix timestamp; // time of received packet - used for timeout char hs_list[MAX_CONNECTIONS]; // list to store all handshake results from clients assigned to this peer int hstimeout; // counts the number of tries the client tried to connect - if reached 10, client put to relay if allowed - int relay; // relay packets by this clients over host + int relay; // relay packets by/to this clients over host } __pack__ peer_list; extern sequence_packet My_Seq; diff --git a/main/network.c b/main/network.c index d99a6ebab..cb7242fee 100644 --- a/main/network.c +++ b/main/network.c @@ -69,6 +69,7 @@ COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED. #include "gamefont.h" #include "songs.h" #include "netdrv.h" +#include "noloss.h" // MWA -- these structures are aligned -- please save me sanity and // headaches by keeping alignment if these are changed!!!! Contact @@ -1955,11 +1956,16 @@ void network_process_packet(ubyte *data, int length ) network_process_naked_pdata((char *)data,length); break; + case PID_PDATA_ACK: + if (Network_status == NETSTAT_PLAYING) + noloss_got_ack(data); + break; + case PID_OBJECT_DATA: if (Network_status == NETSTAT_WAITING) network_read_object_packet(data); break; - + case PID_ENDLEVEL: if ((Network_status == NETSTAT_ENDLEVEL) || (Network_status == NETSTAT_PLAYING)) network_read_endlevel_packet(data); @@ -3094,6 +3100,8 @@ network_start_game() d_srand( timer_get_fixed_seconds() ); Netgame.Security=d_rand(); // For syncing Netgames with player packets + + noloss_init_queue(); if(network_select_players()) { @@ -3409,6 +3417,7 @@ network_level_sync(void) MySyncPackInitialized = 0; network_flush(); // Flush any old packets + noloss_init_queue(); if (N_players == 0) result = network_wait_for_sync(); @@ -3524,8 +3533,10 @@ int network_do_join_game(int choice) network_set_game_mode(Netgame.gamemode); - StartNewLevel(Netgame.levelnum, 0); + noloss_init_queue(); + StartNewLevel(Netgame.levelnum, 0); + return 1; // look ma, we're in a game!!! } @@ -4025,9 +4036,9 @@ void network_do_frame(int force, int listen) // Send out packet PacksPerSec times per second maximum... unless they fire, then send more often... if ( (last_send_time>F1_0/Netgame.PacketsPerSec) || (Network_laser_fired) || force || PacketUrgent ) { + int to_queue = (Network_laser_fired || force || PacketUrgent); if ( Players[Player_num].connected ) { int objnum = Players[Player_num].objnum; - PacketUrgent = 0; if (listen) { multi_send_robot_frame(0); @@ -4097,8 +4108,14 @@ void network_do_frame(int force, int listen) } } } + + noloss_process_queue(); // Check queued packets, remove or re-send if necessery + noloss_add_packet_to_queue(to_queue, MySyncPack.numpackets, MySyncPack.data, MySyncPack.data_size); // add the current packet to queue + + PacketUrgent = 0; MySyncPack.data_size = 0; // Start data over at 0 length. + if (Control_center_destroyed) { if (Player_is_dead) @@ -4204,6 +4221,8 @@ void network_read_pdata_packet(ubyte *data ) TheirPlayernum = pd->playernum; TheirObjnum = Players[pd->playernum].objnum; + noloss_send_ack(pd->numpackets, pd->playernum); + if (TheirPlayernum < 0) { Int3(); // This packet is bogus!! return; @@ -4359,6 +4378,8 @@ void network_read_pdata_short_packet(short_frame_info *pd ) TheirPlayernum = new_pd.playernum; TheirObjnum = Players[new_pd.playernum].objnum; + + noloss_send_ack(new_pd.numpackets, new_pd.playernum); if (TheirPlayernum < 0) { Int3(); // This packet is bogus!! diff --git a/main/network.h b/main/network.h index 4c787ebb3..6b414cd58 100644 --- a/main/network.h +++ b/main/network.h @@ -74,6 +74,7 @@ COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED. // new packet types to get a little bit more information about the netgame so we can show up some rules/flags - uses netgame_info instead of lite_info #define PID_LITE_INFO_D2X 65 // like PID_LITE_INFO #define PID_GAME_LIST_D2X 66 // like PID_GAME_LIST +#define PID_PDATA_ACK 75 // ACK message for a PDATA packet #define NETGAME_ANARCHY 0 #define NETGAME_TEAM_ANARCHY 1 diff --git a/main/noloss.c b/main/noloss.c new file mode 100644 index 000000000..357b35995 --- /dev/null +++ b/main/noloss.c @@ -0,0 +1,227 @@ +/* + * Packet-loss-prevention code for DXX-Rebirth + * This code will save the data field of a netgame packet to a queue. + * Saving is only done for urgent packets. + * Each PDATA packet will be ACK'd by the receiver. + * If all receivers submitted their ACK packets to initial sender, the packet is removed from the queue. + * If a player has not ACK'd a PDATA packet within a second, it will resend to him. + * Timeout, disconnects, or changes in player count are handled as well. + */ + +#include "noloss.h" + +struct pdata_noloss_store noloss_queue[NOLOSS_QUEUE_SIZE]; +extern frame_info MySyncPack; +extern int MaxXDataSize; +extern int N_players; + +// Adds a packet to our queue +// Should be called when an IMPORTANT frameinfo packet is created +void noloss_add_packet_to_queue(int urgent, int pkt_num, char *data, ushort data_size) +{ + int i; + + // Only proceed if this is a version-checked game + if (Netgame.protocol_version != MULTI_PROTO_D2X_VER) + return; + + // Only add urgent packets + if (!urgent) + return; + + for (i = 0; i < NOLOSS_QUEUE_SIZE; i++) + { + if (noloss_queue[i].used) + continue; + + noloss_queue[i].used = 1; + noloss_queue[i].pkt_initial_timestamp = GameTime; + noloss_queue[i].pkt_timestamp = GameTime; + noloss_queue[i].pkt_num = pkt_num; + memset( &noloss_queue[i].player_ack, 0, sizeof(ubyte)*MAX_PLAYERS ); + noloss_queue[i].N_players = N_players; + memcpy( &noloss_queue[i].data[0], data, data_size ); + noloss_queue[i].data_size = data_size; + + return; + } + con_printf(CON_DEBUG, "Noloss queue is full!\n"); +} + +// Send the packet stored in queue list at index to given receiver_pnum +// Called from inside noloss_process_queue() +void noloss_send_queued_packet(int queue_index) +{ + short_frame_info ShortSyncPack; + int objnum = Players[Player_num].objnum; + + // Update Timestamp + noloss_queue[queue_index].pkt_timestamp = GameTime; + // Copy the multibuf data to MySyncPack + memcpy( &MySyncPack.data[0],&noloss_queue[queue_index].data[0], noloss_queue[queue_index].data_size ); + MySyncPack.data_size = noloss_queue[queue_index].data_size; + + // The following code HEAVILY borrows from network_do_frame() + // Create a frameinfo packet + if (Netgame.ShortPackets) + { +#ifdef WORDS_BIGENDIAN + ubyte send_data[MAX_DATA_SIZE]; +#endif + int i; + + memset(&ShortSyncPack,0,sizeof(short_frame_info)); + create_shortpos(&ShortSyncPack.thepos, Objects+objnum, 0); + ShortSyncPack.type = PID_PDATA; + ShortSyncPack.playernum = Player_num; + ShortSyncPack.obj_render_type = Objects[objnum].render_type; + ShortSyncPack.level_num = Current_level_num; + ShortSyncPack.data_size = MySyncPack.data_size; + memcpy (&ShortSyncPack.data[0],&MySyncPack.data[0],MySyncPack.data_size); + + MySyncPack.numpackets = INTEL_INT(noloss_queue[queue_index].pkt_num); + ShortSyncPack.numpackets = MySyncPack.numpackets; +#ifndef WORDS_BIGENDIAN + for(i=0; ireceiver_pnum != Player_num) + return; + + for (i = 0; i < NOLOSS_QUEUE_SIZE; i++) + { + if (gotack->pkt_num == noloss_queue[i].pkt_num) + { + noloss_queue[i].player_ack[gotack->sender_pnum] = 1; + break; + } + } +} + +// Init/Free the queue. Call at start and end of a game or level. +void noloss_init_queue(void) +{ + memset(&noloss_queue,0,sizeof(pdata_noloss_store)*NOLOSS_QUEUE_SIZE); +} + +// The main queue-process function. +// 1) Check if we can remove a packet from queue (all players positive or packet too old) +// 2) Check if there are packets in queue which we need to re-send to player(s) (if packet is older than one second) +void noloss_process_queue(void) +{ + int i; + + for (i = 0; i < NOLOSS_QUEUE_SIZE; i++) + { + int j, resend = 0; + + if (!noloss_queue[i].used) + continue; + + // If N_players differs (because of disconnet), we resend to all to make sure we we get the right ACK packets for the right players + if (N_players != noloss_queue[i].N_players) + { + memset( noloss_queue[i].player_ack, 0, sizeof(ubyte)*8 ); + noloss_queue[i].N_players = N_players; + resend = 1; + } + else + { + // Check if at least one connected player has not ACK'd the packet + for (j = 0; j < N_players; j++) + { + if (!noloss_queue[i].player_ack[j] && Players[j].connected && j != Player_num) + { + resend = 1; + } + } + } + + // Check if we can remove a packet... + if (!resend || + ((noloss_queue[i].pkt_initial_timestamp + (F1_0*15) <= GameTime) || (GameTime < noloss_queue[i].pkt_initial_timestamp))) + { + memset(&noloss_queue[i],0,sizeof(pdata_noloss_store)); + } + // ... otherwise resend if a second has passed + else if ((noloss_queue[i].pkt_timestamp + F1_0 <= GameTime) || (GameTime < noloss_queue[i].pkt_timestamp)) + { + con_printf(CON_DEBUG, "Re-Sending queued packet %i\n",i); + noloss_send_queued_packet(i); + } + } +} diff --git a/main/noloss.h b/main/noloss.h new file mode 100644 index 000000000..2879bb26f --- /dev/null +++ b/main/noloss.h @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "strutil.h" +#include "pstypes.h" +#include "netpkt.h" +#include "network.h" +#include "game.h" +#include "multi.h" +#include "netpkt.h" +#include "netdrv.h" +#include "byteswap.h" + +#define NOLOSS_QUEUE_SIZE 512 // Store up to 512 packets + +// the structure list keeping the data we may want to resend +// this does only contain the extra data field of a PDATA packet (if that isn't enough, the whole PDATA struct info still could be added later) +typedef struct pdata_noloss_store { + int used; + fix pkt_initial_timestamp; // initial timestamp to see if packet is outdated + fix pkt_timestamp; // Packet timestamp + int pkt_num; // Packet number + ubyte player_ack[MAX_PLAYERS]; // 0 if player has not ACK'd this packet, 1 if ACK'd or not connected + int N_players; // Save N_players, too - so if a player disconnects we will resend to make sure all acks are related to the right players + char data[NET_XDATA_SIZE]; // extra data of a packet - contains all multibuf data we don't want to loose + ushort data_size; +} __pack__ pdata_noloss_store; + +// ACK signal packet +typedef struct noloss_ack { + ubyte type; + ubyte sender_pnum; + ubyte receiver_pnum; + int pkt_num; +} __pack__ noloss_ack; + +void noloss_add_packet_to_queue(int urgent, int pkt_num, char *data, ushort data_size); +void noloss_send_ack(int pkt_num, ubyte receiver_pnum); +void noloss_got_ack(ubyte *data); +void noloss_init_queue(void); +void noloss_process_queue(void);