Netcode Overhaul

Rewrote netcode to be more stable and functional. Packet-server mode has been restrategized and will now be the default netmode when playing with 3+ non-LAN players. TryRunTics has been cleaned up with older tick control behavior removed to account for the rewritten renderer. The main thread is now more consistent when playing online to prevent potential slow downs and lock ups. Load barriers are better accounted for to prevent spikes on level transition. Improvements to chat and lobby systems including a force start game button. Added a suite of new host options such as kicking and controlling who can pause the game. Max players increased from 8 to 16 since the new code can now handle it.

Note: Demo functionality is untested. This will be rewritten at a later time alongside improvements to GZDoom's playback features (e.g. freecam mode).
This commit is contained in:
Boondorl 2024-11-24 18:26:06 -05:00 committed by Rachael Alexanderson
commit 94be307225
45 changed files with 3119 additions and 2669 deletions

View file

@ -75,6 +75,7 @@
#include "cmdlib.h"
#include "printf.h"
#include "i_interface.h"
#include "c_cvars.h"
#include "i_net.h"
@ -104,18 +105,31 @@ typedef int SOCKET;
typedef int socklen_t;
#endif
constexpr int MaxPlayers = 16; // TODO: This needs to be put in some kind of unified header later
constexpr size_t MaxPasswordSize = 256u;
bool netgame, multiplayer;
int consoleplayer; // i.e. myconnectindex in Build.
doomcom_t doomcom;
CUSTOM_CVAR(String, net_password, "", CVAR_IGNORE)
{
if (strlen(self) + 1 > MaxPasswordSize)
{
self = "";
Printf(TEXTCOLOR_RED "Password cannot be greater than 255 characters\n");
}
}
FClientStack NetworkClients;
//
// NETWORKING
//
static u_short DOOMPORT = (IPPORT_USERRESERVED + 29);
static SOCKET mysocket = INVALID_SOCKET;
static sockaddr_in sendaddress[MAXNETNODES];
static uint8_t sendplayer[MAXNETNODES];
static sockaddr_in sendaddress[MaxPlayers];
#ifdef __WIN32__
const char *neterror (void);
@ -134,12 +148,20 @@ enum
PRE_ALLHEREACK, // Sent from guest to host to acknowledge PRE_ALLHEREACK receipt
PRE_GO, // Sent from host to guest to continue game startup
PRE_IN_PROGRESS, // Sent from host to guest if the game has already started
PRE_WRONG_PASSWORD, // Sent from host to guest if their provided password was wrong
};
// Set PreGamePacket.fake to this so that the game rejects any pregame packets
// after it starts. This translates to NCMD_SETUP|NCMD_MULTI.
#define PRE_FAKE 0x30
struct PreGameConnectPacket
{
uint8_t Fake;
uint8_t Message;
char Password[MaxPasswordSize];
};
struct PreGamePacket
{
uint8_t Fake;
@ -154,17 +176,16 @@ struct PreGamePacket
{
uint32_t address;
uint16_t port;
uint8_t player;
uint8_t pad;
} machines[MAXNETNODES];
uint16_t pad;
} machines[MaxPlayers];
};
uint8_t TransmitBuffer[TRANSMIT_SIZE];
FString GetPlayerName(int num)
{
if (sysCallbacks.GetPlayerName) return sysCallbacks.GetPlayerName(sendplayer[num]);
else return FStringf("Player %d", sendplayer[num] + 1);
if (sysCallbacks.GetPlayerName) return sysCallbacks.GetPlayerName(num);
else return FStringf("Player %d", num + 1);
}
//
@ -200,22 +221,34 @@ void BindToLocalPort (SOCKET s, u_short port)
I_FatalError ("BindToPort: %s", neterror ());
}
void I_ClearNode(int node)
{
memset(&sendaddress[node], 0, sizeof(sendaddress[node]));
}
static bool I_ShouldStartNetGame()
{
if (doomcom.consoleplayer != 0)
return false;
return StartWindow->ShouldStartNet();
}
int FindNode (const sockaddr_in *address)
{
int i;
int i = 0;
// find remote node number
for (i = 0; i<doomcom.numnodes; i++)
for (; i < doomcom.numplayers; ++i)
{
if (address->sin_addr.s_addr == sendaddress[i].sin_addr.s_addr
&& address->sin_port == sendaddress[i].sin_port)
{
break;
if (i == doomcom.numnodes)
{
// packet is not from one of the players (new game broadcast?)
i = -1;
}
}
return i;
return (i == doomcom.numplayers) ? -1 : i;
}
//
@ -249,8 +282,8 @@ void PacketSend (void)
{
// Printf("send %lu/%d\n", size, doomcom.datalength);
c = sendto(mysocket, (char *)TransmitBuffer, size,
0, (sockaddr *)&sendaddress[doomcom.remotenode],
sizeof(sendaddress[doomcom.remotenode]));
0, (sockaddr *)&sendaddress[doomcom.remoteplayer],
sizeof(sendaddress[doomcom.remoteplayer]));
}
else
{
@ -262,8 +295,8 @@ void PacketSend (void)
{
// Printf("send %d\n", doomcom.datalength);
c = sendto(mysocket, (char *)doomcom.data, doomcom.datalength,
0, (sockaddr *)&sendaddress[doomcom.remotenode],
sizeof(sendaddress[doomcom.remotenode]));
0, (sockaddr *)&sendaddress[doomcom.remoteplayer],
sizeof(sendaddress[doomcom.remoteplayer]));
}
}
// if (c == -1)
@ -315,7 +348,7 @@ void PacketGet (void)
}
else
{
doomcom.remotenode = -1; // no packet
doomcom.remoteplayer = -1; // no packet
return;
}
}
@ -331,7 +364,7 @@ void PacketGet (void)
{
Printf("Net decompression failed (zlib error %s)\n", M_ZLibError(err).GetChars());
// Pretend no packet
doomcom.remotenode = -1;
doomcom.remoteplayer = -1;
return;
}
c = msgsize + 1;
@ -350,11 +383,11 @@ void PacketGet (void)
uint8_t msg[] = { PRE_FAKE, PRE_IN_PROGRESS };
PreSend(msg, 2, &fromaddress);
}
doomcom.remotenode = -1;
doomcom.remoteplayer = -1;
return;
}
doomcom.remotenode = node;
doomcom.remoteplayer = node;
doomcom.datalength = (short)c;
}
@ -373,22 +406,6 @@ sockaddr_in *PreGet (void *buffer, int bufferlen, bool noabort)
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK || (noabort && err == WSAECONNRESET))
return NULL; // no packet
if (doomcom.consoleplayer == 0)
{
int node = FindNode(&fromaddress);
I_NetMessage("Got unexpected disconnect.");
doomcom.numnodes--;
for (; node < doomcom.numnodes; ++node)
sendaddress[node] = sendaddress[node + 1];
// Let remaining guests know that somebody left.
SendConAck(doomcom.numnodes, doomcom.numplayers);
}
else
{
I_NetError("The host disbanded the game unexpectedly");
}
}
return &fromaddress;
}
@ -439,7 +456,7 @@ void BuildAddress (sockaddr_in *address, const char *name)
if (!isnamed)
{
address->sin_addr.s_addr = inet_addr (target.GetChars());
Printf ("Node number %d, address %s\n", doomcom.numnodes, target.GetChars());
Printf ("Node number %d, address %s\n", doomcom.numplayers, target.GetChars());
}
else
{
@ -448,7 +465,7 @@ void BuildAddress (sockaddr_in *address, const char *name)
I_FatalError ("gethostbyname: couldn't find %s\n%s", target.GetChars(), neterror());
address->sin_addr.s_addr = *(int *)hostentry->h_addr_list[0];
Printf ("Node number %d, hostname %s\n",
doomcom.numnodes, hostentry->h_name);
doomcom.numplayers, hostentry->h_name);
}
}
@ -489,17 +506,17 @@ void StartNetwork (bool autoPort)
#endif
}
void SendAbort (void)
void SendAbort (int connected)
{
uint8_t dis[2] = { PRE_FAKE, PRE_DISCONNECT };
int i, j;
if (doomcom.numnodes > 1)
if (connected > 1)
{
if (consoleplayer == 0)
if (doomcom.consoleplayer == 0)
{
// The host needs to let everyone know
for (i = 1; i < doomcom.numnodes; ++i)
for (i = 1; i < connected; ++i)
{
for (j = 4; j > 0; --j)
{
@ -512,7 +529,7 @@ void SendAbort (void)
// Guests only need to let the host know.
for (i = 4; i > 0; --i)
{
PreSend (dis, 2, &sendaddress[1]);
PreSend (dis, 2, &sendaddress[0]);
}
}
}
@ -526,17 +543,17 @@ void SendConAck (int num_connected, int num_needed)
packet.Message = PRE_CONACK;
packet.NumNodes = num_needed;
packet.NumPresent = num_connected;
for (int node = 1; node < doomcom.numnodes; ++node)
for (int node = 1; node < num_connected; ++node)
{
PreSend (&packet, 4, &sendaddress[node]);
}
I_NetProgress (doomcom.numnodes);
I_NetProgress (num_connected);
}
bool Host_CheckForConnects (void *userdata)
{
PreGamePacket packet;
int numplayers = (int)(intptr_t)userdata;
PreGameConnectPacket packet;
int* connectedPlayers = (int*)userdata;
sockaddr_in *from;
int node;
@ -550,7 +567,7 @@ bool Host_CheckForConnects (void *userdata)
{
case PRE_CONNECT:
node = FindNode (from);
if (doomcom.numnodes == numplayers)
if (doomcom.numplayers == *connectedPlayers)
{
if (node == -1)
{
@ -566,13 +583,21 @@ bool Host_CheckForConnects (void *userdata)
{
if (node == -1)
{
node = doomcom.numnodes++;
if (strlen(net_password) > 0 && strcmp(net_password, packet.Password))
{
packet.Message = PRE_WRONG_PASSWORD;
PreSend(&packet, 2, from);
break;
}
node = *connectedPlayers;
++*connectedPlayers;
sendaddress[node] = *from;
I_NetMessage ("Got connect from node %d.", node);
}
// Let the new guest (and everyone else) know we got their message.
SendConAck (doomcom.numnodes, numplayers);
SendConAck (*connectedPlayers, doomcom.numplayers);
}
break;
@ -580,27 +605,25 @@ bool Host_CheckForConnects (void *userdata)
node = FindNode (from);
if (node >= 0)
{
I_ClearNode(node);
I_NetMessage ("Got disconnect from node %d.", node);
doomcom.numnodes--;
while (node < doomcom.numnodes)
--*connectedPlayers;
while (node < *connectedPlayers)
{
sendaddress[node] = sendaddress[node+1];
node++;
++node;
}
// Let remaining guests know that somebody left.
SendConAck (doomcom.numnodes, numplayers);
SendConAck (*connectedPlayers, doomcom.numplayers);
}
break;
case PRE_KEEPALIVE:
break;
}
}
if (doomcom.numnodes < numplayers)
if (*connectedPlayers < doomcom.numplayers && !I_ShouldStartNetGame())
{
// Send message to everyone as a keepalive
SendConAck(doomcom.numnodes, numplayers);
SendConAck(*connectedPlayers, doomcom.numplayers);
return false;
}
@ -614,53 +637,53 @@ bool Host_CheckForConnects (void *userdata)
node = FindNode (from);
if (node >= 0)
{
doomcom.numnodes--;
while (node < doomcom.numnodes)
I_ClearNode(node);
--*connectedPlayers;
while (node < *connectedPlayers)
{
sendaddress[node] = sendaddress[node+1];
node++;
++node;
}
// Let remaining guests know that somebody left.
SendConAck (doomcom.numnodes, numplayers);
SendConAck (*connectedPlayers, doomcom.numplayers);
}
break;
}
}
return doomcom.numnodes >= numplayers;
// TODO: This will need a much better solution later.
if (I_ShouldStartNetGame())
doomcom.numplayers = *connectedPlayers;
return *connectedPlayers >= doomcom.numplayers;
}
bool Host_SendAllHere (void *userdata)
{
int *gotack = (int *)userdata; // ackcount is at gotack[MAXNETNODES]
int* gotack = (int*)userdata;
const int mask = (1 << doomcom.numplayers) - 1;
PreGamePacket packet;
int node;
sockaddr_in *from;
// Send out address information to all guests. Guests that have already
// acknowledged receipt effectively get just a heartbeat packet.
// Send out address information to all guests. This is will be the final order
// of the send addresses so each guest will need to rearrange their own in order
// to keep node -> player numbers synchronized.
packet.Fake = PRE_FAKE;
packet.Message = PRE_ALLHERE;
for (node = 1; node < doomcom.numnodes; node++)
for (node = 1; node < doomcom.numplayers; node++)
{
int machine, spot = 0;
packet.ConsoleNum = node;
if (!gotack[node])
if (!((*gotack) & (1 << node)))
{
for (spot = 0, machine = 1; machine < doomcom.numnodes; machine++)
for (spot = 0; spot < doomcom.numplayers; spot++)
{
if (node != machine)
{
packet.machines[spot].address = sendaddress[machine].sin_addr.s_addr;
packet.machines[spot].port = sendaddress[machine].sin_port;
packet.machines[spot].player = node;
spot++; // fixes problem of new address replacing existing address in
// array; it's supposed to increment the index before getting
// and storing in the packet the next address.
}
packet.machines[spot].address = sendaddress[spot].sin_addr.s_addr;
packet.machines[spot].port = sendaddress[spot].sin_port;
}
packet.NumNodes = doomcom.numnodes - 2;
packet.NumNodes = doomcom.numplayers;
}
else
{
@ -672,23 +695,36 @@ bool Host_SendAllHere (void *userdata)
// Check for replies.
while ( (from = PreGet (&packet, sizeof(packet), false)) )
{
if (packet.Fake == PRE_FAKE && packet.Message == PRE_ALLHEREACK)
if (packet.Fake != PRE_FAKE)
continue;
if (packet.Message == PRE_ALLHEREACK)
{
node = FindNode (from);
if (node >= 0)
*gotack |= 1 << node;
}
else if (packet.Message == PRE_CONNECT)
{
// If someone is still trying to connect, let them know it's either too
// late or that they need to move on to the next stage.
node = FindNode(from);
if (node == -1)
{
if (!gotack[node])
{
gotack[node] = true;
gotack[MAXNETNODES]++;
}
packet.Message = PRE_ALLFULL;
PreSend(&packet, 2, from);
}
else
{
packet.Message = PRE_CONACK;
packet.NumNodes = packet.NumPresent = doomcom.numplayers;
PreSend(&packet, 4, from);
}
PreSend (&packet, 2, from);
}
}
// If everybody has replied, then this loop can end.
return gotack[MAXNETNODES] == doomcom.numnodes - 1;
return ((*gotack) & mask) == mask;
}
bool HostGame (int i)
@ -696,24 +732,24 @@ bool HostGame (int i)
PreGamePacket packet;
int numplayers;
int node;
int gotack[MAXNETNODES+1];
if ((i == Args->NumArgs() - 1) || !(numplayers = atoi (Args->GetArg(i+1))))
{ // No player count specified, assume 2
numplayers = 2;
}
if (numplayers > MAXNETNODES)
if (numplayers > MaxPlayers)
{
I_FatalError("You cannot host a game with %d players. The limit is currently %d.", numplayers, MAXNETNODES);
I_FatalError("You cannot host a game with %d players. The limit is currently %d.", numplayers, MaxPlayers);
}
if (numplayers == 1)
{ // Special case: Only 1 player, so don't bother starting the network
NetworkClients += 0;
netgame = false;
multiplayer = true;
doomcom.id = DOOMCOM_ID;
doomcom.numplayers = doomcom.numnodes = 1;
doomcom.numplayers = 1;
doomcom.consoleplayer = 0;
return true;
}
@ -723,27 +759,41 @@ bool HostGame (int i)
// [JC] - this computer is starting the game, therefore it should
// be the Net Arbitrator.
doomcom.consoleplayer = 0;
Printf ("Console player number: %d\n", doomcom.consoleplayer);
doomcom.numnodes = 1;
doomcom.numplayers = numplayers;
I_NetInit ("Hosting game", numplayers);
// Wait for numplayers-1 different connections
if (!I_NetLoop (Host_CheckForConnects, (void *)(intptr_t)numplayers))
// Wait for the lobby to be full.
int connectedPlayers = 1;
if (!I_NetLoop (Host_CheckForConnects, (void *)&connectedPlayers))
{
SendAbort();
SendAbort(connectedPlayers);
throw CExitEvent(0);
return false;
}
// If the player force started with only themselves in the lobby, start the game
// immediately.
if (doomcom.numplayers <= 1)
{
NetworkClients += 0;
netgame = false;
multiplayer = true;
doomcom.id = DOOMCOM_ID;
doomcom.numplayers = 1;
I_NetDone();
return true;
}
// Now inform everyone of all machines involved in the game
memset (gotack, 0, sizeof(gotack));
int gotack = 1;
I_NetMessage ("Sending all here.");
I_NetInit ("Done waiting", 1);
if (!I_NetLoop (Host_SendAllHere, (void *)gotack))
if (!I_NetLoop (Host_SendAllHere, (void *)&gotack))
{
SendAbort();
SendAbort(doomcom.numplayers);
throw CExitEvent(0);
return false;
}
@ -751,7 +801,7 @@ bool HostGame (int i)
I_NetMessage ("Go");
packet.Fake = PRE_FAKE;
packet.Message = PRE_GO;
for (node = 1; node < doomcom.numnodes; node++)
for (node = 1; node < doomcom.numplayers; node++)
{
// If we send the packets eight times to each guest,
// hopefully at least one of them will get through.
@ -761,16 +811,10 @@ bool HostGame (int i)
}
}
I_NetMessage ("Total players: %d", doomcom.numnodes);
I_NetMessage ("Total players: %d", doomcom.numplayers);
doomcom.id = DOOMCOM_ID;
doomcom.numplayers = doomcom.numnodes;
// On the host, each player's number is the same as its node number
for (i = 0; i < doomcom.numnodes; ++i)
{
sendplayer[i] = i;
}
return true;
}
@ -782,16 +826,18 @@ bool Guest_ContactHost (void *userdata)
{
sockaddr_in *from;
PreGamePacket packet;
PreGameConnectPacket sendPacket;
// Let the host know we are here.
packet.Fake = PRE_FAKE;
packet.Message = PRE_CONNECT;
PreSend (&packet, 2, &sendaddress[1]);
sendPacket.Fake = PRE_FAKE;
sendPacket.Message = PRE_CONNECT;
memcpy(sendPacket.Password, net_password, strlen(net_password) + 1);
PreSend (&sendPacket, sizeof(sendPacket), &sendaddress[0]);
// Listen for a reply.
while ( (from = PreGet (&packet, sizeof(packet), true)) )
{
if (packet.Fake == PRE_FAKE && FindNode(from) == 1)
if (packet.Fake == PRE_FAKE && !FindNode(from))
{
if (packet.Message == PRE_CONACK)
{
@ -812,6 +858,10 @@ bool Guest_ContactHost (void *userdata)
{
I_NetError("The game was already started.");
}
else if (packet.Message == PRE_WRONG_PASSWORD)
{
I_NetError("Invalid password.");
}
}
}
@ -828,7 +878,7 @@ bool Guest_WaitForOthers (void *userdata)
while ( (from = PreGet (&packet, sizeof(packet), false)) )
{
if (packet.Fake != PRE_FAKE || FindNode(from) != 1)
if (packet.Fake != PRE_FAKE || FindNode(from))
{
continue;
}
@ -839,31 +889,35 @@ bool Guest_WaitForOthers (void *userdata)
break;
case PRE_ALLHERE:
if (doomcom.numnodes == 2)
if (doomcom.consoleplayer == -1)
{
int node;
doomcom.numnodes = packet.NumNodes + 2;
sendplayer[0] = packet.ConsoleNum; // My player number
doomcom.numplayers = packet.NumNodes;
doomcom.consoleplayer = packet.ConsoleNum;
// Don't use the address sent from the host for our own machine.
sendaddress[doomcom.consoleplayer] = sendaddress[1];
I_NetMessage ("Console player number: %d", doomcom.consoleplayer);
for (node = 0; node < packet.NumNodes; node++)
for (int node = 1; node < doomcom.numplayers; ++node)
{
sendaddress[node+2].sin_addr.s_addr = packet.machines[node].address;
sendaddress[node+2].sin_port = packet.machines[node].port;
sendplayer[node+2] = packet.machines[node].player;
if (node == doomcom.consoleplayer)
continue;
sendaddress[node].sin_addr.s_addr = packet.machines[node].address;
sendaddress[node].sin_port = packet.machines[node].port;
// [JC] - fixes problem of games not starting due to
// no address family being assigned to nodes stored in
// sendaddress[] from the All Here packet.
sendaddress[node+2].sin_family = AF_INET;
sendaddress[node].sin_family = AF_INET;
}
I_NetMessage("Received All Here, sending ACK.");
}
I_NetMessage ("Received All Here, sending ACK.");
packet.Fake = PRE_FAKE;
packet.Message = PRE_ALLHEREACK;
PreSend (&packet, 2, &sendaddress[1]);
PreSend (&packet, 2, &sendaddress[0]);
break;
case PRE_GO:
@ -876,10 +930,6 @@ bool Guest_WaitForOthers (void *userdata)
}
}
packet.Fake = PRE_FAKE;
packet.Message = PRE_KEEPALIVE;
PreSend(&packet, 2, &sendaddress[1]);
return false;
}
@ -892,33 +942,32 @@ bool JoinGame (int i)
StartNetwork (true);
// Host is always node 1
BuildAddress (&sendaddress[1], Args->GetArg(i+1));
sendplayer[1] = 0;
doomcom.numnodes = 2;
// Host is always node 0
BuildAddress (&sendaddress[0], Args->GetArg(i+1));
doomcom.numplayers = 2;
doomcom.consoleplayer = -1;
// Let host know we are here
I_NetInit ("Contacting host", 0);
if (!I_NetLoop (Guest_ContactHost, NULL))
if (!I_NetLoop (Guest_ContactHost, nullptr))
{
SendAbort();
SendAbort(2);
throw CExitEvent(0);
return false;
}
// Wait for everyone else to connect
if (!I_NetLoop (Guest_WaitForOthers, 0))
if (!I_NetLoop (Guest_WaitForOthers, nullptr))
{
SendAbort();
SendAbort(2);
throw CExitEvent(0);
return false;
}
I_NetMessage ("Total players: %d", doomcom.numnodes);
I_NetMessage ("Total players: %d", doomcom.numplayers);
doomcom.id = DOOMCOM_ID;
doomcom.numplayers = doomcom.numnodes;
return true;
}
@ -954,19 +1003,20 @@ static int PrivateNetOf(in_addr in)
static bool NodesOnSameNetwork()
{
int net1;
if (doomcom.consoleplayer != 0)
return false;
net1 = PrivateNetOf(sendaddress[1].sin_addr);
const int firstClient = PrivateNetOf(sendaddress[1].sin_addr);
// Printf("net1 = %08x\n", net1);
if (net1 == 0)
if (firstClient == 0)
{
return false;
}
for (int i = 2; i < doomcom.numnodes; ++i)
for (int i = 2; i < doomcom.numplayers; ++i)
{
int net = PrivateNetOf(sendaddress[i].sin_addr);
const int net = PrivateNetOf(sendaddress[i].sin_addr);
// Printf("Net[%d] = %08x\n", i, net);
if (net != net1)
if (net != firstClient)
{
return false;
}
@ -1004,6 +1054,8 @@ int I_InitNetwork (void)
Printf ("using alternate port %i\n", DOOMPORT);
}
net_password = Args->CheckValue("-password");
// parse network game options,
// player 1: -host <numplayers>
// player x: -join <player 1's address>
@ -1018,19 +1070,22 @@ int I_InitNetwork (void)
else
{
// single player game
NetworkClients += 0;
netgame = false;
multiplayer = false;
doomcom.id = DOOMCOM_ID;
doomcom.numplayers = doomcom.numnodes = 1;
doomcom.ticdup = 1;
doomcom.numplayers = 1;
doomcom.consoleplayer = 0;
return false;
}
if (doomcom.numnodes < 3)
if (doomcom.numplayers < 3)
{ // Packet server mode with only two players is effectively the same as
// peer-to-peer but with some slightly larger packets.
return false;
}
return doomcom.numnodes > 3 || !NodesOnSameNetwork();
return !NodesOnSameNetwork();
}
@ -1070,7 +1125,7 @@ void I_NetMessage(const char* text, ...)
void I_NetError(const char* error)
{
doomcom.numnodes = 0;
doomcom.numplayers = 0;
StartWindow->NetClose();
I_FatalError("%s", error);
}