Added Packet-loss prevention code for Netgames
This commit is contained in:
parent
28b1ccbb52
commit
4d59723cc2
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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))
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!!
|
||||
|
|
|
@ -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
|
||||
|
|
227
main/noloss.c
Normal file
227
main/noloss.c
Normal file
|
@ -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; i<N_players; i++)
|
||||
{
|
||||
// Check if a player is not connected anymore so we won't send a packet and set positive
|
||||
if (!noloss_queue[queue_index].player_ack[i] && !Players[i].connected)
|
||||
noloss_queue[queue_index].player_ack[i] = 1;
|
||||
|
||||
if(Players[i].connected && (i != Player_num))
|
||||
netdrv_send_packet_data((ubyte*)&ShortSyncPack, sizeof(short_frame_info) - MaxXDataSize + MySyncPack.data_size, NetPlayers.players[i].network.ipx.server, NetPlayers.players[i].network.ipx.node,Players[i].net_address);
|
||||
}
|
||||
#else
|
||||
squish_short_frame_info(ShortSyncPack, send_data);
|
||||
|
||||
for(i=0; i<N_players; i++) {
|
||||
// Check if a player is not connected anymore so we won't send a packet and set positive
|
||||
if (!noloss_queue[queue_index].player_ack[i] && !Players[i].connected)
|
||||
noloss_queue[queue_index].player_ack[i] = 1;
|
||||
|
||||
if(!noloss_queue[queue_index].player_ack[i] && Players[i].connected && (i != Player_num))
|
||||
netdrv_send_packet_data((ubyte*)send_data, IPX_SHORT_INFO_SIZE-MaxXDataSize+MySyncPack.data_size, NetPlayers.players[i].network.ipx.server, NetPlayers.players[i].network.ipx.node,Players[i].net_address);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
else // If long packets
|
||||
{
|
||||
int i;
|
||||
|
||||
MySyncPack.numpackets = noloss_queue[queue_index].pkt_num;
|
||||
MySyncPack.type = PID_PDATA;
|
||||
MySyncPack.playernum = Player_num;
|
||||
MySyncPack.obj_render_type = Objects[objnum].render_type;
|
||||
MySyncPack.level_num = Current_level_num;
|
||||
MySyncPack.obj_segnum = Objects[objnum].segnum;
|
||||
MySyncPack.obj_pos = Objects[objnum].pos;
|
||||
MySyncPack.obj_orient = Objects[objnum].orient;
|
||||
MySyncPack.phys_velocity = Objects[objnum].mtype.phys_info.velocity;
|
||||
MySyncPack.phys_rotvel = Objects[objnum].mtype.phys_info.rotvel;
|
||||
|
||||
for(i=0; i<N_players; i++)
|
||||
{
|
||||
// Check if a player is not connected anymore so we won't send a packet and set positive
|
||||
if (!noloss_queue[queue_index].player_ack[i] && !Players[i].connected)
|
||||
noloss_queue[queue_index].player_ack[i] = 1;
|
||||
|
||||
if(!noloss_queue[queue_index].player_ack[i] && Players[i].connected && (i != Player_num))
|
||||
{
|
||||
send_frameinfo_packet(&MySyncPack, NetPlayers.players[i].network.ipx.server, NetPlayers.players[i].network.ipx.node,Players[i].net_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MySyncPack.data_size = 0; // Start data over at 0 length.
|
||||
}
|
||||
|
||||
// We have received a PDATA packet. Send ACK response to sender!
|
||||
// ACK packet needs to contain: packet num, sender player num, receiver player num
|
||||
// Call in network_process_packet() at case PID_PDATA
|
||||
void noloss_send_ack(int pkt_num, ubyte receiver_pnum)
|
||||
{
|
||||
noloss_ack ack;
|
||||
|
||||
memset(&ack,0,sizeof(noloss_ack));
|
||||
|
||||
ack.type = PID_PDATA_ACK;
|
||||
ack.sender_pnum = Player_num;
|
||||
ack.receiver_pnum = receiver_pnum;
|
||||
ack.pkt_num = pkt_num;
|
||||
|
||||
netdrv_send_packet_data( (ubyte *)&ack, sizeof(noloss_ack), NetPlayers.players[receiver_pnum].network.ipx.server, NetPlayers.players[receiver_pnum].network.ipx.node,Players[receiver_pnum].net_address );
|
||||
}
|
||||
|
||||
// We got an ACK by a player. Set this player slot to positive!
|
||||
void noloss_got_ack(ubyte *data)
|
||||
{
|
||||
int i;
|
||||
noloss_ack *gotack = (noloss_ack *)data;
|
||||
|
||||
if (gotack->receiver_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);
|
||||
}
|
||||
}
|
||||
}
|
42
main/noloss.h
Normal file
42
main/noloss.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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);
|
Loading…
Reference in a new issue